spec: archive compact portfolio totals

This commit is contained in:
2026-09-09 10:32:27 +03:00
parent b06a35bbaa
commit d5c18258c4
15 changed files with 4270 additions and 5 deletions
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-09
@@ -0,0 +1,214 @@
## Context
Страница Brokerage Account уже разделяет получение данных в
`AccountPage.vm.ts` и отображение в `AccountPageLoaded.view.tsx`.
`PortfolioTotals` получает готовые `totalsByCurrency`; текущая сводка использует
Mantine `SimpleGrid` и `Paper`, вертикальную метку ISO над суммой, отступ 16 px
и верхнюю синюю границу 3 px. Максимальная ширина контента страницы — 960 px.
`shared/lib/display.ts` уже форматирует decimal string без числового
преобразования: сохраняет знак и дробные позиции, группирует целую часть.
`formatMoney` меняет только RUB на ₽. Query и VM уже сохраняют успешные данные
при refetch/error; `PortfolioRefreshState` отделён от server-side `isStale`.
Действующий initial skeleton содержит общий блок высотой 120 px вместо
геометрии валютных карточек.
Основание подхода — утверждённые [Proposal](proposal.md) и
[Delta Specs](specs/portfolio-view/spec.md). A1 из Discovery выбран владельцем;
ни выбор, ни прежние PNG сами по себе не были approval Design.
## Goals / Non-Goals
**Goals:** изменить представление сводки и её loading placeholder в существующем
page slice; сохранить строковую точность и валютную идентичность; использовать
нынешние primitives, theme tokens и границы VM/view.
**Non-Goals:** менять финансовые вычисления, транспортные типы, query/cache
policy, state machine, общие правила денежных суффиксов приложения или соседние
разделы страницы. Новые зависимости, инструментальные витрины и сервисы
форматирования не нужны.
## Decisions
### Ownership и поток данных
`PortfolioTotals` остаётся владельцем компактного представления каждого
переданного итога. Порядок, membership и ключи берутся из исходных валют;
никакой повторной агрегации, сортировки по знакам или дополнения валют нет.
VM продолжает выбирать loaded/empty по составу Portfolio, а не по итогам.
Пустой список итогов не создаёт карточек или отдельного empty-состояния.
Loaded view сохраняет своё место сводки после времени снимка и перед составом.
Его timestamp, refresh/error и stale sections продолжают получать прежние
props. Меняется только сводка и соответствующая область page-owned skeleton.
Это удерживает ответственность за устойчивость данных в query/VM, а за
геометрию — в presentation.
### Точное форматирование и валютная идентичность
Переиспользовать существующий `formatDecimal`, открыв его через публичный
`shared/lib` export без изменения алгоритма и поведения `formatMoney`.
Выбор метки и суффикса принадлежит сводке: RUB → ₽, USD → $, EUR → €;
для остальных валют метка и суффикс — исходный ISO-код. Например, USD/CAD
дают `$`/`CAD`, поэтому совпадающие мировые знаки не требуют каталога валют
или анализа всей коллекции. Неизвестные валюты не отбрасываются.
Полные названия трёх валют принадлежат существующему русскому i18n-каталогу.
Семантика карточки — пара название/значение (`dl`, `dt`, `dd` либо эквивалентная
связь): доступное название содержит «Российский рубль», «Доллар США» или «Евро»;
для прочих валют доступен ISO-код. Декоративный badge скрыт от accessibility
tree, полное текстовое значение остаётся доступным. Карточка не кнопка,
не получает tab stop и не требует tooltip/hover для определения валюты.
Числа не проходят через `Number`, `parseFloat`, округление или currency-mode
`Intl.NumberFormat`. Пробелы группировки и десятичная запятая сохраняются;
перенос — CSS layout, а не вставка символов в числовые данные. `-0.5` остаётся
`-0,5`, исходные дробные нули сохраняются. Замена ISO-кода в уже собранной строке
`formatMoney` отвергнута: она связывала бы сводку с деталями суффикса общего
formatter. Новый дубликат decimal formatter также не нужен.
### Компоновка средствами платформы
Mantine `Paper` и page-local CSS остаются основой карточек. Сетка сохраняет
колонки `base: 1`, `sm: 2`, `md: 3` (48 em / 62 em), с gap 10 px.
Одна валюта занимает одну ячейку сетки, как в A1; дополнительные валюты
заполняют следующие строки, без фиктивных пустых карточек.
Внутри карточки метка и сумма образуют переносимую строку. Естественная ширина
полного текста определяет вместимость, без JS-измерений и порогов по количеству
цифр. При недостатке места сумма переходит ниже метки и получает всю доступную
ширину. Ограничения `min-width: 0`, `max-width: 100%`, обычные переносы на
пробелах и `overflow-wrap: anywhere` защищают также длинную дробную часть.
Не задаются фиксированная высота, `nowrap`, ellipsis, clipping или уменьшение
шрифта ради вместимости. На переносимой строке знак минуса не отделяется
от первой цифры; остальные цифры и суффикс остаются текстом в исходном порядке.
Альтернативы: единая общая полоса не соответствует выбранным отдельным карточкам;
фиксированная двухстрочная раскладка теряет компактность обычных значений;
обрезание/скролл/округление нарушают Delta Specs. Нативный reflow сохраняет
простую архитектуру и работает при увеличении текста.
## UI/UX Applicability and Package
**Applicability and scope:** `new/changed` — сводка и её initial skeleton на
`/accounts/:accountId`. Навигация, heading, время, позиции, Cash Balance и
остальные секции на PNG служат контекстом; их внешний вид не является заданием
на редизайн. Наличие в runtime дополнительных секций не является расхождением.
**Package revision:** `a1-symbols-r1`. Один approval относится ко всему этому
Markdown и восьми локальным PNG ниже. Они доступны без Pencil, Storybook,
исходного чата или внешнего аккаунта.
**Reused source and differences:** это новый canonical пакет, а не ссылка на
approved archived reuse. PNG перенесены byte-for-byte из выбранных Discovery r3
exports; их первоначальные имена перечислены ниже. Исходный `.pen` не является
зависимостью пакета. Markdown дополняет изображения правилами для других валют,
accessibility, промежуточных ширин и произвольно длинных значений.
### Визуальная геометрия
Карточка белая, граница gray-3 1 px со всех сторон, radius 8 px, без отдельной
синей верхней полосы и тени. Padding 12 px; промежуток метка/сумма 10 px.
Слева badge: фон blue-0, текст blue-6, radius 4 px, минимум 40 × 24 px;
для ISO-кодов ширина растёт по тексту. Знак 18 px / 600. Сумма — 20 px / 600,
line-height 1.55, основной цвет dark-7. Используется системный font stack
приложения, без загрузки Roboto только ради совпадения canvas.
Обычная строка выравнивает сумму к правому краю. После перехода под метку
сумма использует всю ширину с чтением слева направо. Предпочтительны переносы
на пробелах группировки; длинная непрерывная дробь может переноситься между
цифрами. Точная точка переноса зависит от системного шрифта: PNG не задаёт
обязательный разрыв после конкретной цифры. Все цифры, минус и суффикс читаемы.
При базовом масштабе обычная карточка ориентировочно 57 px, три карточки
на mobile — 191 px вместе с двумя gap. Это design geometry, а не runtime
замер: критерием служат полные значения и уменьшение высоты относительно
прежнего UI при одинаковых ширине, шрифте, масштабе и данных. Длинные значения
и увеличение текста закономерно увеличивают высоту.
### Screens, states and interactions
| Состояние | Решение |
| --- | --- |
| Loaded, 1/3 валюты | Отдельные A1 карточки; символ слева, полная сумма справа при вместимости. |
| Loaded, больше валют / неизвестная валюта | Та же сетка без лимита количества; прочие валюты используют ISO-код в badge и суффиксе. |
| Длинные / отрицательные значения | Сумма уходит под badge и переносится; `-0,5 $` сохраняет минус; зелёный/красный тон по знаку не добавляется. |
| Initial loading | Три декоративных card placeholders с badge/amount shapes, в той же responsive сетке; число shapes не обещает три валюты. Существующий `LoadingSkeleton` владеет busy label, shapes скрыты от чтения и не фокусируются. Остальные semantic regions loading view сохраняются. |
| Refetch | Прежние карточки и `lastSuccessfulAt` плюс существующий compact progress; карточки не заменяются skeleton. |
| Client refresh error | Прежние значения и время плюс существующий orange warning и «Повторить». |
| Server stale | Прежний blue warning и время; само `isStale` не создаёт retry. |
| Error + stale | Оба самостоятельных предупреждения, карточки и время; retry принадлежит клиентской ошибке. |
| Retry → success | До ответа сохраняются значения; успешный ответ обновляет значения/время, снимает client error; stale зависит только от нового ответа. |
| Нет итогов при непустом составе | Нет карточек; состав и время остаются. |
| Пустые позиции и Cash Balance | Существующее empty-представление, без нулевых карточек. |
| Ошибка без успешных данных | Существующая error view; `404` ведёт к списку счетов без retry; ошибки подключения/токена сохраняют свои действия, временная/неизвестная ошибка допускает retry. |
Новых взаимодействий нет. Кнопка повтора сохраняет клавиатурную доступность
и существующую callback-связь с VM. Статические суммы не превращаются в live
region; обновление сообщает существующий status region.
**Responsive behavior:** на 320/390 px одна карточка в ряду; при 1440 px и
контенте 960 px — три. Между ними действуют указанные breakpoints. При любом
числе колонок переполнение значения переводит его ниже метки, а не расширяет
grid track. Одна валюта на узком экране занимает всю ширину. Loading использует
те же правила; refetch/error/stale не меняют геометрию самих итогов.
### Materials
| Material ID (Discovery source) | Relative path | Screen/state and purpose | SHA-256 |
| --- | --- | --- | --- |
| wide (ACV5F) | [visuals/a1-wide.png](visuals/a1-wide.png) | 1440, три обычных итога | `b406552e01098a5b95183d6a54bd59b3bf827387d33d73a6a453d83140b8da10` |
| narrow (j6ks4) | [visuals/a1-narrow.png](visuals/a1-narrow.png) | 390, три обычных итога | `8ee71bb406abaa980e08f57c45e007fb66183786dfd251c9280a6ff62205d922` |
| single (iwyJA) | [visuals/a1-single-wide.png](visuals/a1-single-wide.png) | 1440, одна валюта | `6f163dd4a4ba461df5e715bab5757f4698986aade56ae38f02585d9c8d366b28` |
| long-wide (Hbu1s) | [visuals/a1-long-wide.png](visuals/a1-long-wide.png) | 1440, длинные значения | `ecccaee124c72b0f4f22bd92b31ef97e260c830782761ed9e7aab4c33a7e0af0` |
| long-320 (vjlkn) | [visuals/a1-long-320.png](visuals/a1-long-320.png) | 320, перенос и сохранение всех цифр | `48d62eaa497819e6312f5ae56afa393f92c7bb0970543cd9b3abdd5960480534` |
| loading (DuAlU) | [visuals/a1-loading-narrow.png](visuals/a1-loading-narrow.png) | 390, placeholders сводки | `138b000b334f54294f6cd8e88d607619608b05620231267fc1dc0477e1f87f46` |
| refresh (aP2cU) | [visuals/a1-refresh-narrow.png](visuals/a1-refresh-narrow.png) | 390, сохранение данных при обновлении | `e453606105a96290a2d2358e039935c89187a90127ffe4228374ae6e16c9e252` |
| error-stale (z5EeB) | [visuals/a1-error-stale-narrow.png](visuals/a1-error-stale-narrow.png) | 390, отдельные предупреждения и retry | `c63c8a30cc932582f591422dd4871c6663cb55567218cee988d7119d77d4a4e3` |
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | A1 totals и соответствующий skeleton; точное форматирование с локальными знаками и доступной идентичностью; сохранённые query/VM состояния. |
| backend | `N/A` | Готовые итоги, расчёты, Snapshot и HTTP/OpenAPI contract не меняются; серверная реализация не требуется. |
| specs | `affected` | Canonical planning, переносимый Design package и нормативное описание `portfolio-view` в рамках lifecycle. |
| docs | `affected` | После реализации `features/portfolio-view.md` описывает фактическую компактную сводку, валютные знаки и ограничения оценки. |
### Contract Gate Decision
| Gate | Status | Reason |
| --- | --- | --- |
| runtime-openapi | `N/A` | Ни одна сторона API contract не меняется; существующие decimal strings и исходные currency остаются входом представления. |
## Risks / Trade-offs
- Разные системные шрифты и zoom меняют вместимость. Нужен browser reflow review
обычных и длинных fixtures на узких и широких экранах; PNG не доказывает
отсутствие runtime overflow или доступность. Точность и полнота важнее высоты.
- Открытие существующего decimal formatter немного расширяет публичный
`shared/lib`, зато исключает дублирование денежных преобразований. Риск
защищается сохранением текущего `formatMoney` и regression cases для точных
цифр, дробных нулей, `-0.5` и валютной идентичности на presentation boundary.
- Значение `$` без доступного полного названия неоднозначно; связь пары
название/значение проверяется в accessibility tree. Цвет не несёт идентичность.
- Placeholder не знает количества валют заранее. Три формы повторяют ожидаемую
структуру, а успешный ответ определяет реальное число карточек.
- Геометрия и денежное представление требуют scoped acceptance проверки;
состояние refetch/error проверяется через существующие account-page tests,
без новой state machine или отдельной инфраструктуры тестирования.
## Migration Plan
После approval Design и затем Tasks представление и loading geometry
поставляются совместно с проверкой точности и сохранённых состояний. После
проверенной реализации docs фиксирует as-built поведение; canonical спецификация
синхронизируется в рамках штатного lifecycle. Миграции данных, переключатель
функции и координированный релиз backend не нужны.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `N/A` |
| Reason | Изменение обратимого frontend presentation без изменения auth, данных, API или другого труднообратимого решения. Обычный откат frontend delivery возвращает прежний вид без преобразования данных. |
@@ -0,0 +1,55 @@
## Why
Сводка итогов Portfolio на странице Brokerage Account занимает избыточную
высоту, особенно на узком экране, и отдаляет пользователя от состава портфеля.
Нужно сделать её компактнее, сохранив читаемость каждой суммы и понимание того,
к какой валюте она относится. В Discovery владелец выбрал направление A1:
отдельные компактные валютные карточки со знаками ₽, $ и €.
## What Changes
- Представить итоги Portfolio компактными отдельными карточками по валютам,
следуя выбранному направлению A1. На широком и узком экране обычная сводка
занимает меньше высоты, сохраняя читаемость и доступность значений.
- Для российского рубля, доллара США и евро показывать ₽, $ и € соответственно
в валютных метках и рядом с суммами вместо RUB, USD и EUR. Сохранить
однозначность валюты и её полное доступное название; другие валюты остаются
различимыми и не исключаются из сводки.
- Показывать длинные, дробные и отрицательные суммы полностью: без потери
цифр или знака, округления, сокращений до тысяч или миллионов, обрезания
и необходимости горизонтальной прокрутки. На узком экране допускать перенос
и увеличение высоты ради полного значения.
- Сохранить смысл каждой суммы: итог позиций и доступных денег в одной валюте,
без сложения разных валют и конвертации. Отсутствующие суммы не заменяются
нулями, рублёвый итог не становится общей стоимостью Portfolio, а сводка
не обещает полноту оценки при отсутствии стоимости отдельных позиций.
- Сохранить существующие состояния: первичное ожидание без вымышленных сумм;
прежние суммы и время успешного получения при фоновом обновлении и его
ошибке; раздельные сообщения об устаревшем снимке и ошибке обновления,
с применимым действием повтора. Существующее пустое состояние сохраняется.
Вне scope: изменение денежных расчётов, состава или источников данных;
редизайн навигации, заголовка, позиций и соседних разделов; изменение
форматирования денег во всём приложении. Выбор A1 задаёт направление этой
сводки; детальное визуальное решение подлежит отдельному согласованию Design.
## Capabilities
### New Capabilities
Нет.
### Modified Capabilities
- `specs/portfolio-view/spec.md`: добавить требования к компактному
представлению валютных итогов на странице Brokerage Account, символам
валют, доступной идентичности и полному отображению сумм на широком
и узком экране при сохранении существующего смысла итогов и состояний.
## Impact
Изменение затрагивает пользователей, просматривающих Portfolio одного
Brokerage Account на широких и узких экранах, и ограничено сводкой его
валютных итогов. При поставке обновляются пользовательское представление,
его нормативное описание и документация фактически доступного поведения.
Способ получения данных и финансовый смысл итогов сохраняются.
@@ -0,0 +1,251 @@
## ADDED Requirements
### Requirement: Компактная сводка валютных итогов Portfolio
На странице Brokerage Account клиент SHALL показывать каждый полученный
валютный итог Portfolio отдельной компактной карточкой. При достаточной
ширине карточки валютная метка и сумма MUST располагаться в одной строке;
сумма MUST оставаться основной читаемой величиной. Обычная сводка с одной
или тремя валютами MUST занимать меньше высоты, чем прежнее представление
с меткой над суммой при тех же значениях и ширине экрана.
На широком экране несколько карточек SHALL располагаться рядом при наличии
места; на узком экране MUST сохраняться одна карточка в ряду. Если полное
значение не помещается рядом с меткой, клиент MUST предоставлять ему полную
доступную ширину карточки и переносить содержимое с увеличением высоты.
Сводка MUST NOT обрезать суммы, заменять их многоточием или требовать
горизонтальной прокрутки для чтения карточек и значений.
#### Scenario: Обычная сводка на широком экране
- **WHEN** User открывает Portfolio с итогами `125000.50` RUB, `320.00` USD
и `9876.54321` EUR на широком экране с местом для трёх карточек
- **THEN** клиент показывает три отдельные карточки рядом, метку и полную
сумму в одной строке каждой карточки, с меньшей высотой сводки, чем при
прежнем размещении метки над суммой в тех же условиях
#### Scenario: Обычная сводка на узком экране
- **WHEN** User открывает тот же Portfolio с тремя валютными итогами
на узком экране
- **THEN** клиент показывает три карточки по одной в ряду, сохраняя полные
читаемые суммы и меньшую общую высоту сводки, чем при прежнем размещении
метки над суммой в тех же условиях
#### Scenario: Portfolio с одной валютой
- **WHEN** Portfolio содержит один валютный итог
- **THEN** клиент показывает одну компактную карточку без пустых карточек
для других валют; обычная сумма и метка помещаются в одну строку при
достаточной ширине на широком и узком экране
#### Scenario: Длинная сумма на узком экране
- **WHEN** User просматривает итог `123456789012.345678901` RUB при ширине
экрана 320 пикселей и значение не помещается рядом с валютной меткой
- **THEN** сумма получает полную доступную ширину карточки и при необходимости
переносится; все цифры и знак валюты читаемы без обрезания, многоточия
и горизонтальной прокрутки
### Requirement: Обозначение и доступная идентичность валюты в сводке
В валютных метках и суффиксах сумм сводки клиент MUST обозначать RUB знаком
`₽`, USD знаком `$`, EUR знаком `€` вместо видимых ISO-кодов этих валют.
Каждая сумма MUST оставаться однозначно связанной со своей исходной валютой,
включая чтение вспомогательными технологиями. Для RUB, USD и EUR MUST быть
доступны названия «Российский рубль», «Доллар США» и «Евро» соответственно.
Символ или цвет сами по себе MUST NOT быть единственным доступным способом
определения валюты.
Другие полученные валюты MUST оставаться в сводке с понятным обозначением
и доступной идентичностью. Если знак отсутствует или неоднозначен, клиент
MUST сохранять возможность различить исходную валюту по её названию или
ISO-коду, в том числе визуально; неизвестное полное название MUST NOT
подменяться вымышленным. Совпадающие знаки разных валют MUST NOT приводить
к объединению карточек или неразличимому представлению их валют.
#### Scenario: Рубль, доллар США и евро показаны знаками
- **WHEN** сводка содержит RUB, USD и EUR
- **THEN** метки показывают `₽`, `$` и `€`, а суммы заканчиваются теми же
знаками; видимые RUB, USD и EUR не подменяют выбранные знаки
#### Scenario: Валютная сумма читается вспомогательной технологией
- **WHEN** User читает валютный итог сводки вспомогательной технологией
- **THEN** ему доступны полное значение и однозначная валютная идентичность,
включая «Российский рубль», «Доллар США» или «Евро» для соответствующих валют
#### Scenario: Получена другая валюта
- **WHEN** сводка содержит валюту помимо RUB, USD и EUR
- **THEN** её отдельный итог остаётся доступным с понятным обозначением
и доступным названием либо исходным ISO-кодом при неизвестном названии
#### Scenario: У разных валют совпадает знак
- **WHEN** Portfolio содержит итоги двух исходных валют с одинаковым знаком
- **THEN** сводка показывает два отдельных итога, и User может различить
их валюты визуально и вспомогательной технологией без догадки по сумме
### Requirement: Полные точные значения и смысл валютной сводки
Сводка MUST показывать полученные валютные итоги без изменения их знака,
цифр и дробной точности. Для русской локали клиент SHALL сохранять группировку
разрядов пробелами и десятичную запятую, не округляя значение, не отбрасывая
дробные позиции и не сокращая сумму до тысяч, миллионов или экспоненциальной
записи. Требование MUST сохраняться для длинных, отрицательных значений
и отрицательных дробей с нулевой целой частью.
Каждая карточка MUST представлять только полученный итог позиций и Cash
Balance своей исходной валюты. Сводка MUST NOT конвертировать или складывать
разные валюты, показывать рублёвый итог как общую стоимость Portfolio,
повторно добавлять Cash Balance либо обещать полную оценку при наличии
позиций без вычислимой стоимости. Для отсутствующего валютного итога клиент
MUST NOT создавать нулевую карточку. Отсутствие итогов само по себе MUST NOT
обозначать пустой Portfolio, если позиции или Cash Balance присутствуют.
#### Scenario: Обычные суммы сохраняют дробные позиции
- **WHEN** в сводку поступают значения `125000.50` RUB, `320.00` USD
и `9876.54321` EUR
- **THEN** клиент показывает `125 000,50 ₽`, `320,00 $` и `9 876,54321 €`
с сохранением каждой исходной цифры и дробной позиции
#### Scenario: Длинные и отрицательные значения не теряют точность
- **WHEN** в сводку поступают `123456789012.345678901` RUB,
`-1234567.890123` USD и `987654321.000001` EUR
- **THEN** пользователь видит полные `123 456 789 012,345678901 ₽`,
`-1 234 567,890123 $` и `987 654 321,000001 €` без округления,
сокращений и потери отрицательного знака на широком и узком экране
#### Scenario: Отрицательная сумма меньше единицы
- **WHEN** полученный валютный итог равен `-0.5` USD
- **THEN** сводка показывает `-0,5 $`, сохраняя знак и дробную величину
#### Scenario: Несколько валют и денежные остатки
- **WHEN** полученные итоги RUB и USD уже включают позиции и Cash Balance
соответствующих валют
- **THEN** клиент показывает эти два итога без повторного добавления денег,
межвалютного итога или представления RUB как общей стоимости Portfolio
#### Scenario: Стоимость части позиций неизвестна
- **WHEN** Portfolio содержит позиции без вычислимой стоимости и доступные
валютные итоги, сформированные без этих позиций
- **THEN** сводка показывает только полученные итоги, не добавляет нули
за отсутствующие стоимости и не называет результат полной оценкой Portfolio
#### Scenario: Итоги отсутствуют при непустом Portfolio
- **WHEN** `totalsByCurrency` пуст, но `positions` или `cashBalances` непусты
- **THEN** клиент не создаёт валютные карточки и не заменяет доступный
состав Portfolio состоянием пустого портфеля
## MODIFIED Requirements
### Requirement: Состояния и ошибки Portfolio на клиенте
Клиент SHALL загружать Portfolio при открытии защищённого маршрута
`/accounts/:accountId`. Он MUST использовать успешный ответ в течение
30 секунд, без automatic retry, polling или refetch при возврате фокуса.
При загрузке без ранее полученного успешного ответа для этого же Brokerage
Account клиент MUST показывать структурное skeleton-состояние ожидаемого
содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST
иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным
или получать клавиатурный фокус.
Истечение срока свежести MUST NOT само по себе скрывать уже доступный
Client Cached Snapshot. Во время фонового обновления и после его ошибки
клиент MUST сохранять ранее полученные данные того же Portfolio, включая
валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST
показывать компактную неблокирующую индикацию без замены данных skeleton.
При ошибке обновления он MUST показывать локализованное неблокирующее
предупреждение о последних полученных данных и предоставлять действие
повторной попытки. Сохранённые данные MUST NOT считаться новым успешным
снимком или подтверждением текущей доступности Brokerage Account.
Клиент MUST показать штатное empty-состояние только при пустых `positions`
и `cashBalances`, без нулевых валютных карточек. Для показываемого состава
Portfolio клиент MUST сохранять время успешного получения снимка
`lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST
показывать Snapshot, его исходное время и нейтральное предупреждение
о временной недоступности T-Bank. Это предупреждение само по себе MUST NOT
добавлять действие ручного обновления. Server-side stale и ошибка фонового
обновления Client Cached Snapshot MUST оставаться раздельными состояниями:
ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения
и действие повтора клиентского обновления MUST оставаться доступными при
одновременном наличии этих состояний.
При отсутствии ранее успешно полученного Client Cached Snapshot этого же
Portfolio и ответе `404` клиент MUST показать состояние недоступного счёта
с действием возврата к списку счетов без действия повтора. При отсутствии
Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE`
или иной ошибке клиент MUST показать безопасное error-состояние с доступными
для ошибки действиями; действие повтора разрешено только для временной
и неизвестной ошибки.
#### Scenario: Показ устаревшего Portfolio
- **WHEN** клиент получает успешный ответ с `isStale: true`
- **THEN** клиент показывает позиции, остатки, валютные итоги, время
последнего успешного снимка и нейтральное предупреждение; само это
предупреждение не добавляет действие ручного обновления
#### Scenario: Недоступный счёт на клиенте
- **WHEN** для текущего Portfolio отсутствует ранее успешно полученный
Client Cached Snapshot и клиент получает `404`
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку
Brokerage Account без действия повтора
#### Scenario: Первичная загрузка Portfolio
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее
полученного успешного ответа
- **THEN** он показывает структурный skeleton, включая место сводки,
с доступным обозначением загрузки, без вымышленных или нулевых сумм
и без фокусируемых placeholders
#### Scenario: Обновление после истечения срока свежести
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое
обновление данных того же Brokerage Account
- **THEN** клиент сохраняет прежние валютные итоги и время их успешного
получения, показывает неблокирующую индикацию обновления и не заменяет
доступный состав и сводку skeleton-состоянием
#### Scenario: Ошибка обновления после успешного ответа
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio,
а обновление этих же данных завершается ошибкой
- **THEN** клиент сохраняет данные, суммы и время прежнего снимка,
показывает неблокирующее предупреждение и действие повтора;
ошибка не делает сохранённый снимок свежим и не изменяет `isStale`
#### Scenario: Ошибка клиентского обновления при server-side stale
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое
обновление этого же Portfolio завершается ошибкой
- **THEN** прежние суммы и время остаются видимыми одновременно
с нейтральным server-side предупреждением, отдельным предупреждением
об ошибке клиентского обновления и его действием повтора
#### Scenario: Повторное обновление успешно
- **WHEN** после ошибки фонового обновления User запускает повторную
попытку и получает успешный Portfolio Snapshot
- **THEN** до ответа прежние суммы и время остаются видимыми, затем
клиент показывает значения и `lastSuccessfulAt` успешного ответа,
убирает предупреждение об ошибке клиентского обновления и показывает
server-side предупреждение только в соответствии с новым `isStale`
#### Scenario: Штатный пустой Portfolio на клиенте
- **WHEN** успешно полученный Portfolio содержит пустые `positions`
и `cashBalances`
- **THEN** клиент показывает существующее empty-состояние без валютных
карточек с вымышленными нулевыми итогами
@@ -0,0 +1,32 @@
## 1. Frontend: компактная сводка и сохранение состояний
Основа — утверждённый [Design](design.md), включая пакет `a1-symbols-r1`
и все восемь PNG из его Materials. Перед первой Node-командой активировать
frontend `.nvmrc` через `nvm use --silent`. Зависимости устанавливать из lockfile
через `npm ci`; новую инфраструктуру и зависимости не добавлять.
- [x] 1.1 [frontend] Открыть существующий `formatDecimal` через публичный `shared/lib` и использовать его в сводке с локальными знаками ₽/$/€, ISO-fallback и доступными названиями валют из i18n, сохранив поведение общего `formatMoney`.
- **Covers:** `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`
- **Verification:** Scoped Vitest через `npm run test:run -- src/shared/lib/display.test.ts src/pages/account`: полные обычные и длинные decimal strings из Delta Specs, дробные нули, отрицательные значения и `-0.5`; видимые знаки и доступные полные названия RUB/USD/EUR; раздельные USD/CAD и неизвестный ISO; прежние результаты `formatMoney` вне сводки. Дополнить поведенческие тесты на существующей presentation boundary без копии алгоритма formatter в assertions.
- [x] 1.2 [frontend] Реализовать A1 в `PortfolioTotals` и page-local CSS: отдельные карточки, badge и сумма в строке при вместимости, responsive сетка и полный перенос длинной суммы под метку; сохранить исходные membership, порядок и смысл итогов.
- **Covers:** `portfolio-view` — `Компактная сводка валютных итогов Portfolio`; `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`
- **Verification:** `npm run test:run -- src/pages/account`: одна/три/больше валют, отсутствие фиктивных карточек, отсутствие повторного добавления Cash Balance или общего межвалютного итога; при неизвестной стоимости позиции показываются только переданные суммы, без обещания полной оценки. Проверить семантическую связь названия с полным значением и отсутствие tab stop у карточки. Реальный reflow проверяется в 1.4, а не выводится из jsdom.
- [x] 1.3 [frontend] Заменить область сводки в page-owned initial skeleton тремя декоративными A1 placeholders и дополнить существующие account-page regression checks для сохранённых данных и состояний, не меняя query/VM policy.
- **Covers:** `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`
- **Verification:** `npm run test:run -- src/pages/account src/entities/portfolio`: initial busy label без сумм и фокусируемых shapes; refetch → error → retry → success сохраняет прежние суммы и UTC `lastSuccessfulAt` до успешного ответа; server stale отдельно и вместе с client error; `404` с/без Client Cached Snapshot; штатные действия ошибок подключения/токена/временной и неизвестной ошибки; empty по positions/cashBalances и непустой состав без totals. Переиспользовать имеющиеся assertions cache policy: 30 секунд, без automatic retry/polling/focus refetch; добавлять только недостающее покрытие затронутых инвариантов.
- [x] 1.4 [frontend] Проверить реализованный пакет в браузере, исправить расхождения в пределах Design и выполнить canonical frontend gate.
- **Covers:** `portfolio-view` — `Компактная сводка валютных итогов Portfolio`; `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
- **Verification:** В существующей локальной витрине/приложении сравнить сводку и loading с `a1-symbols-r1` на 320, 390 и 1440 px и по обе стороны 48/62 em; использовать синтетические обычные, длинные, отрицательные значения, одну/три/дополнительные валюты и zoom 200%. Проверить все цифры и знаки, получение полной ширины под меткой, отсутствие обрезания и горизонтального overflow сводки (`scrollWidth <= clientWidth` для сводки/карточек), меньшую высоту обычных одной/трёх валют относительно прежнего UI при одинаковых данных, ширине, шрифте и масштабе. Проверить accessibility tree, клавиатурный retry, initial/refetch/error/stale и неизменность соседних sections. Сохранить observed measurements/screenshots как implementation evidence, не как новую revision Design. Затем после lockfile install выполнить `npm run verify` из frontend; runtime-openapi не требуется по Design.
## 2. Документация и canonical пакет
- [x] 2.1 [docs] После проверенной frontend реализации обновить `features/portfolio-view.md` описанием фактической компактной сводки, валютных знаков, точных переносимых сумм и ограничений оценки; сохранить согласованное описание состояний.
- **Covers:** `portfolio-view` — `Компактная сводка валютных итогов Portfolio`; `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
- **Verification:** Scoped review соответствия документа реализованному UI и применимых локальных ссылок; из docs после `nvm use --silent` и `npm ci` выполнить `npm run verify`. Не переносить planning или будущие verification results в as-built документацию.
- [x] 2.2 [specs] Подготовить к поставке полный canonical change package: утверждённые planning artifacts, восемь Materials `a1-symbols-r1` и фактические отметки завершения implementation Tasks; сохранить неизменными approval-bound содержимое и относительную структуру пакета.
- **Covers:** `N/A` — задача не вводит отдельного пользовательского поведения; specs владеет поставкой уже подготовленного normative/UI пакета и checklist progress согласно Delivery Map. Main-spec sync и archive остаются последующей Finalization.
- **Verification:** Из specs под активированным frontend runtime выполнить `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate compact-portfolio-totals --type change --strict --store wealth-ledger-specs` и `openspec validate --specs --strict --store wealth-ledger-specs`. Сверить полный native Delta membership, prerequisite approval fingerprints и Tasks fingerprint с нормализацией только checklist progress; проверить все пути/SHA-256 материалов из Design. Не создавать независимый PASS, Completion Evidence или archive в этой task.
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB