Commit Graph

72 Commits

Author SHA1 Message Date
ZProger
848ff21665 feat: профили импорта CSV для Т-Банка, monobank и ПриватБанка, версия 1.1.8 2026-09-13 06:32:36 +03:00
ZProger
c867710f38 fix: перевести SEO-теги и document lang на английский 2026-09-13 00:42:51 +03:00
ZProger
3bd484b88e feat: добавить SEO-теги (description, keywords, Open Graph, Twitter Card) 2026-09-13 00:42:51 +03:00
ZProger
14948aff1d security: TLS, проверка Host, шифрованное запоминание входа и аудит-лог
- TLS через подключаемый overlay docker-compose.tls.yml + Caddyfile:
  Caddy сам получает и продлевает сертификат Let's Encrypt, HSTS
  добавляется на уровне прокси — намеренно там, а не в nginx.conf,
  иначе установки без TLS заперли бы сами себя;
- проверка заголовка Host против DNS rebinding: AURUM_ALLOWED_HOSTS,
  по умолчанию localhost и 127.0.0.1, чужой Host получает 421 с
  подсказкой, какую переменную поправить;
- «запомнить меня на 7 дней» вернулось в безопасном виде: пароль
  шифруется AES-GCM ключом с extractable: false в IndexedDB, вместо
  прежнего base64 в localStorage, который разворачивался обратно в
  пароль одним atob. Требует secure context, поэтому по обычному HTTP
  к LAN-адресу чекбокс не показывается, а не притворяется рабочим;
- аудит-лог необратимых операций (восстановление бэкапа, массовое
  создание транзакций, удаление счёта) в лог контейнера. Не в БД
  сознательно: таблица аудита исчезла бы ровно в том событии, которое
  должна объяснять;
- README: раздел про TLS и таблица двух переменных, решающих, кто
  может достучаться до инстанса.
2026-09-10 21:34:40 +03:00
ZProger
f9413aa6e1 security: устранить находки аудита, версия 1.1.7
Конфигурация и эксплуатационный слой:
- nginx: client_max_body_size 64m — восстановление из бэкапа не работало
  вовсе, дефолтный лимит в 1 МБ резал дампы (реальный экспорт ~3.4 МБ)
  с 413 ещё до backend;
- nginx: заголовки безопасности (nosniff, Referrer-Policy,
  Permissions-Policy) и полный Content-Security-Policy. Ради него
  inline-скрипт темы вынесен в public/theme-init.js;
- nginx: limit_req на /api/ — перебор пароля Basic Auth шёл со
  скоростью сети, ни блокировки, ни задержки не было;
- nginx: server_tokens off;
- порт web привязан к 127.0.0.1, адрес настраивается через
  AURUM_BIND_ADDRESS (раньше слушался на всех интерфейсах);
- backend работает от непривилегированного пользователя,
  no-new-privileges для backend и web;
- дефолты пароля Postgres сведены к одному значению в обоих сервисах,
  в .env.example описана ловушка с инициализацией тома.

Приложение:
- версия убрана из неавторизованного /api/health и отдаётся как
  app_version в GET /api/settings;
- /api/docs, /api/redoc и /api/openapi.json отключаются через
  AURUM_ENABLE_DOCS;
- текст исключения при неудачном восстановлении уходит в лог, а не
  клиенту;
- max_length для notes в схемах транзакций, активов и регулярных
  платежей (и в соответствующих полях UI);
- срок «запомнить меня» сокращён с 30 дней до 7;
- pytest обновлён до 9.0.3 (PYSEC-2026-1845), pytest-asyncio до 1.4.0;
- CI: permissions contents:read для GITHUB_TOKEN.
2026-09-10 21:12:31 +03:00
ZProger
eba8add92f fix: устранить баги в проверке дублей и смене кодировки при импорте CSV
- Кэш проверки дублей не сбрасывался после импорта: повторный импорт
  того же файла в течение 30 секунд (staleTime) сверялся со списком
  транзакций, полученным ДО первого импорта, и не находил ни одного
  дубля — ровно тот случай, ради которого проверка и делалась.
- Кнопку импорта можно было нажать, пока проверка дублей ещё шла или
  шёл фоновый рефетч: дубли проходили мимо фильтра. Теперь кнопка
  заблокирована на время запроса, а его ошибка больше не проглатывается
  молча, а показывается предупреждением.
- Ручная смена кодировки ломала уже выбранные колонки: заголовки
  перечитывались, старый выбор указывал на несуществующее имя,
  indexOf давал -1, и все строки молча отбрасывались как «не удалось
  распознать дату». Маппинг переугадывается при изменении состава
  заголовков.
- Текст «Они не будут импортированы» противоречил включённой галочке
  «Всё равно импортировать их».
2026-09-10 20:16:00 +03:00
ZProger
777e3af725 feat: улучшить импорт CSV — дедупликация, кодировка, парсинг сумм
Пять доработок импорта транзакций из CSV:
1. Проверка дублей при повторном импорте — сверка с уже существующими
   транзакциями на счёте за диапазон дат файла (по дате+типу+сумме+
   описанию), похожие исключаются из импорта по умолчанию с опцией
   всё равно их добавить.
2. Автоопределение кодировки файла (UTF-8 с откатом на Windows-1251)
   вместо жёстко зашитого UTF-8 — раньше кириллица из банковских
   выгрузок превращалась в кракозябры. Есть ручной выбор на случай
   неверного автоопределения.
3. Исправлен баг в parseAmount: суммы с несколькими разделителями
   тысяч и без дробной части (например "1,234,567") теряли цифры и
   отбрасывались как нераспознанные — теперь распознаются верно.
4. Добавлен явный выбор разделителя дробной части (точка/запятая) на
   случай, когда автоопределение не может разрешить неоднозначность.
5. Пример значения из файла показывается прямо под каждым выбором
   колонки при сопоставлении — не нужно доходить до превью, чтобы
   понять, что в колонке.
2026-09-10 20:06:41 +03:00
ZProger
6df4e57f08 feat: заменить баннер отсутствия auth на модальное окно с согласием
Дизайн-баннер был легко закрываем и появлялся заново каждую сессию.
Теперь показывается блокирующее модальное окно без кнопки закрытия/
Escape/клика по фону — пройти дальше можно только явным подтверждением
'Я понимаю риски'. Согласие сохраняется в localStorage и не запрашивается повторно.
2026-09-10 19:38:07 +03:00
ZProger
292ef1e410 feat: показывать баннер, если инстанс не защищён паролем
Раньше предупреждение об отсутствии AURUM_BASIC_AUTH_USER/PASSWORD
было видно только в логах docker-контейнера при старте и терялось,
если пользователь забыл настроить .env. Теперь LoginGate различает
'авторизован' и 'auth вообще не настроен' и во втором случае
показывает заметный баннер прямо в интерфейсе.
2026-09-10 19:34:25 +03:00
ZProger
19ca9c3158 fix: скрывать file input бэкапа без display:none
На Firefox (Linux) программный .click() по input[type=file] со стилем
display:none может не открывать нативный диалог выбора файла. Теперь
инпут скрыт визуально (opacity + нулевой размер), но остаётся в layout.
2026-09-10 19:29:39 +03:00
ZProger
cd2e4ae9f9 fix: заменить crypto.randomUUID() на fallback для HTTP без TLS
crypto.randomUUID() доступен только в secure context (HTTPS/localhost),
из-за чего при открытии приложения по обычному HTTP падал TypeError
при добавлении сплитов транзакции.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01769tkxLJooEWNjx3xc9aCv
2026-09-10 19:17:20 +03:00
ZProger
225b46dc61 build(deps): обновить vite, @vitejs/plugin-react, react-router-dom, @types/node, @types/react-dom
Закрывает Dependabot PR #23, #26, #29, #30, #31 — все minor/patch,
билд и тесты зелёные.
2026-09-09 01:20:42 +03:00
ZProger
12a3068bee Merge branch 'db-26' 2026-09-09 01:19:49 +03:00
ZProger
31471a14ec Merge branch 'db-31' 2026-09-09 01:19:49 +03:00
dependabot[bot]
a27b35febf build(deps-dev): bump @types/react-dom in /frontend
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 19.2.4 to 19.2.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-08 22:06:48 +00:00
dependabot[bot]
7647eabdfa build(deps-dev): bump @types/node from 26.2.0 to 26.4.1 in /frontend
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 26.2.0 to 26.4.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.4.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-08 22:06:43 +00:00
dependabot[bot]
bc4f861a78 build(deps-dev): bump vite from 8.2.1 to 8.2.2 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.2.1 to 8.2.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.2.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-08 22:06:26 +00:00
ZProger
f5c6591310 test: юнит-тесты на форматирование и разбивки крипто-портфеля + версия 1.1.6
Vitest для чистой логики: lib/format.ts (заодно поймал баг в
trimTrailingZeros на "-0.000") и расчёт разбивки по риску/монете/сети
на вкладке Crypto — вынес их из компонентов в отдельные тестируемые
функции. Тесты подключены в CI. Версия приложения обновлена до 1.1.6.
2026-09-09 00:52:27 +03:00
ZProger
c3047c6d6a feat: сеть (network) для крипто-холдингов
Свободное текстовое поле на холдинге — какой сети принадлежит конкретная
монета (Ethereum, Tron и т.д.), с редактированием прямо в таблице и
разбивкой % по сетям на отдельной вкладке. Помогает увидеть, что часть
стейблкоинов лежит на рискованных сетях и стоит перевести на безопасные.
2026-09-09 00:52:11 +03:00
ZProger
526bfe104d fix: неоднозначные символы валют (CN¥, HK$) без огрубления остальных
currencyDisplay: narrowSymbol вместо code — чинит CNY/HKD, но не
превращает $/€/₽ в USD/EUR/RUB для всех остальных валют.
2026-09-09 00:32:22 +03:00
ZProger
8b87619e37 fix: контент растягивается на всю ширину при свёрнутом меню
Убрал фиксированный max-w-6xl у основного контейнера — раньше
освободившееся при сворачивании сайдбара место просто пропадало.
2026-09-08 21:46:14 +03:00
ZProger
fcbd217915 feat: добавить уровень риска для крипто-холдингов
Риск (низкий/средний/высокий) задаётся при добавлении монеты и
редактируется прямо в таблице холдингов. Добавлена вкладка "Уровни
риска" в карточке портфеля с разбивкой % по риск-тирам, только для
крипты. Заодно поправлено округление процентов в риск-карточках.
2026-09-08 21:46:09 +03:00
whaley
12c3a7d79c add "debit_card" as a new AccountType (#16) 2026-09-08 20:03:13 +03:00
ZProger
c0f446b3b3 fix: копеечные цены крипты не округлялись в $0 2026-09-04 02:37:09 +03:00
ZProger
bd97e316cc fix: сортировать активы в «Криптовалюте» по вложенной сумме
Монеты в таблице активов шли в алфавитном порядке (сортировка по
имени в SQL-запросе) — с импортированными портфелями это выглядело
хаотично. Теперь список отсортирован по cost basis (сколько денег
реально вложено в монету) от большего к меньшему; полностью
проданная позиция уходит в конец списка, а не в начало.
2026-09-04 02:26:37 +03:00
ZProger
cd125e9e45 fix: график крипты открывать за всё время, лучший/худший рост — по выбранному периоду
График во вкладке «Криптовалюта» теперь по умолчанию показывает всю
историю, а не 30 дней — короткое окно у портфеля, растущего годами,
часто попадало на локальную просадку и выглядело демотивирующе.

Плитки «Лучший рост» и «Худшее падение» раньше всегда считались за
24 часа. Теперь они следуют за диапазоном, выбранным у графика
(7д/30д/90д/всё время): для 7д/30д используются закэшированные данные
CoinGecko, для «всё время» — метрика за 1 год (дальше бесплатный
тариф CoinGecko историю не отдаёт), а для 90д — отдельный запрос по
каждой монете, который выполняется только когда пользователь реально
выбрал этот период.
2026-09-04 02:18:31 +03:00
ZProger
aefd6cc035 feat: портфели в разделе «Криптовалюта»
Криптовалюту теперь можно разделять на отдельные портфели (например,
долгосрочные инвестиции, накопления на недвижимость, спекулятивные
монеты) вместо одного общего списка — по тому же принципу, что уже
есть у счетов (Account): у портфеля есть имя, цветная метка и
архивация, каждая монета обязательно принадлежит одному портфелю,
один и тот же токен можно держать в нескольких портфелях сразу.
2026-09-04 01:39:18 +03:00
ZProger
c2f59c8773 fix: учитывать инвестсчета как наличные в расчёте капитала
Тип счёта investment не входил в CASH_ACCOUNT_TYPES, из-за чего деньги
на инвестсчёте не попадали ни в блок "Наличные", ни в класс "Инвестиции"
раздела Капитал — просто выпадали из расчёта net worth (issue #14).
2026-09-03 23:29:27 +03:00
ZProger
946b78d989 feat: показывать версию приложения в настройках, версия 1.1.0
Версии бэкенда и фронтенда были не синхронизированы и нигде не отображались
в интерфейсе — непонятно было, обновилось ли приложение после пересборки.
Теперь единая версия 1.1.0 приходит с бэкенда через /api/health и мелким
текстом выводится внизу страницы настроек.
2026-09-02 02:21:05 +03:00
Pirnazar Muradov
c422fd1528 fix: force LF line endings so containers start on Windows checkouts (#11)
Cloning on Windows with core.autocrlf=true converted
frontend/docker-entrypoint.d/20-basic-auth.sh to CRLF. Inside the container
the kernel read the shebang as /bin/sh\r, found no such interpreter, and
nginx crashed with exit 127 in a restart loop, so `docker compose up` never
produced a working web container.

- .gitattributes: LF for everything Linux executes or parses
- frontend/Dockerfile: sed strips CR at build time, covering checkouts made
  before .gitattributes existed
2026-09-02 02:08:35 +03:00
ZProger
da1c7b5d63 feat: allow deleting default categories once they have no transactions
Раньше стандартные категории нельзя было удалить вообще. Теперь можно —
если у категории нет ни одной обычной транзакции, ни строки сплита с
ней. Пользовательские категории удаляются как и раньше, без проверок.

- delete_category() проверяет Transaction.category_id и
  TransactionSplit.category_id для стандартных категорий перед
  удалением.
- Кнопка удаления в списке категорий больше не блокируется заранее для
  стандартных — бэкенд решает, бейдж «стандартная» остался чисто
  информационным.
- При отказе (400) показывается понятное сообщение вместо тихого
  игнорирования клика.
2026-08-30 19:49:21 +03:00
ZProger
dbae7938da fix: show full category path (parent · subcategory) everywhere, group filters by hierarchy
Транзакция на подкатегории показывала только её имя — непонятно было,
что это подкатегория и к какой родительской категории она относится.
Фильтры по категории в Транзакциях и Отчётах тоже перечисляли
подкатегории наравне с родительскими, без иерархии.

- categoryLabels.ts: categoryPath() и buildHierarchicalCategories() —
  общая логика, вынесенная из TransactionFormModal.
- Список транзакций и «Последние транзакции» на Dashboard показывают
  счёт, затем родителя, затем подкатегорию.
- Фильтр категорий в Транзакциях сгруппирован по типу (как в Отчётах);
  оба списка показывают подкатегорию с отступом под родителем.
2026-08-30 19:30:49 +03:00
ZProger
87b37a10ff fix: move breakdown button to the start of the row, swap chevron for SquareDivide
Резерв под стрелку справа от суммы отъедал место у самой суммы даже
когда у категории нет подкатегорий — суммы во всей колонке смотрелись
сдвинутыми от правого края. Кнопка перенесена в начало строки, где
резерв под неё не мешает колонке сумм. Иконка заменена на SquareDivide
(та же, что уже используется для сплит-транзакций в списке).
2026-08-30 19:20:00 +03:00
ZProger
84dc8e9721 fix: reserve fixed-width chevron slot so category amounts stay aligned
Стрелка-шеврон появлялась только у строк с дочерними категориями, из-за
чего сумма в такой строке сдвигалась влево относительно остальных.
Теперь под стрелку всегда резервируется одинаковое место (пустое, если
разбивки нет), и суммы во всех строках выравниваются по одному краю.
2026-08-30 19:15:31 +03:00
ZProger
b4503a933f fix: show subcategory breakdown in a modal instead of expanding inline
Инлайн-раскрытие внутри строки при большом числе подкатегорий/сплитов
раздувало высоту строки и ломало вёрстку (донат на Dashboard и список
справа расходились по высоте). Теперь клик по шеврону открывает общее
модальное окно CategoryBreakdownModal со списком категорий-источников —
строка сама больше не меняет размер.
2026-08-30 19:13:13 +03:00
ZProger
140f58d33f fix: restrict transaction splits to one parent category, show subcategory breakdown
Раньше сплит позволял смешивать вообще любые категории. Реальная задача —
разложить одну покупку по подкатегориям ОДНОЙ базовой категории (100 € на
продукты: сколько на сладости, сколько на уход). А раз Dashboard и рейтинг
категорий сворачивают подкатегории в родителя, эта разбивка нигде не была
видна.

- _build_splits() требует, чтобы все категории сплита были одной родительской
  категорией или её прямыми подкатегориями.
- category_rollup.py отдаёт для каждого родительского тотала список
  category-источников, которые в него скинулись (children) — пусто, если
  источник один.
- Dashboard и рейтинг категорий: строка с непустым children раскрывается по
  клику, показывая вклад каждой подкатегории.
- Форма транзакции: выбор категории при сплите — сначала базовая (верхний
  уровень), затем строки сплита выбирают саму базовую или её подкатегорию.
2026-08-30 19:06:29 +03:00
ZProger
0134cdb6bf feat: split a transaction across multiple categories
Один чек часто смешивает несколько категорий трат (гипермаркет: часть — продукты,
часть — бытовая химия). Раньше такую транзакцию приходилось огрублять до одной
категории или дробить вручную на несколько записей. Теперь у транзакции есть
второй режим: 2+ доли (TransactionSplit) с суммами по разным категориям, которые
обязаны в точности складываться в сумму транзакции.

- Новая модель TransactionSplit + миграция.
- split_rule_violation() проверяет инварианты на создании и редактировании.
- category_rollup.py — общий helper, которым теперь пользуются Dashboard, отчёт
  по категориям и бюджеты, учитывая доли сплитов наравне с обычными транзакциями.
- Фильтр по категории в списке транзакций находит и сплит-транзакции.
- Сплиты попадают в полный бэкап/восстановление БД.
- 13 новых backend-тестов.
- Фронтенд: переключатель «Разбить на несколько категорий» в форме транзакции
  с повторяемыми строками и индикатором остатка суммы; строка транзакции в
  списке показывает все категории сплита.
2026-08-30 18:42:13 +03:00
ZProger
7deacd2a98 fix: quantity/price fields padded with zeros when editing a transaction
The API returns quantity and price_per_unit at their full Numeric(38,18)
scale (e.g. "3.000000000000000000") since crypto quantities need up to 18
decimal places for wei-level amounts — correct for computation, but the
edit form pre-filled these raw strings straight into the inputs.

Added trimTrailingZeros() to lib/format.ts and used it when pre-filling
CryptoTransactionModal in edit mode. Trims the string directly rather than
round-tripping through Number(), which would risk rounding a genuine
18-decimal-place amount through floating point instead of just stripping
padding.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 05:10:29 +03:00
ZProger
2ad39f9780 fix: collapsed-sidebar logo expands the menu; crypto nav icon matches the rest
Collapsed sidebar had no way to expand except the button at the bottom —
the logo at the top was purely decorative. It's now a real button in that
state (expanded state unchanged, still non-interactive there).

Swapped the Crypto nav icon from Bitcoin (a filled coin-badge glyph) to
Coins — same thin line-icon style as the rest of the sidebar (Layers,
Target, Flag, ...), where Bitcoin's brand-logo shape stood out. Bitcoin
stays as-is elsewhere (holdings table row fallback, Net Worth breakdown) —
appropriate there as a specific-asset placeholder, this was only about the
nav item.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 05:08:41 +03:00
ZProger
c3309b9d60 feat: click a coin row to view its trade history; edit transactions
Matches CoinMarketCap's flow: clicking anywhere on a coin's row in the
holdings table opens its transaction history (previously only a dedicated
icon button did). The table's own History icon is gone since the row now
does that job; the remaining "+"/delete icons get stopPropagation so they
don't also trigger the row click.

Adds the missing piece: transactions could be viewed and deleted but not
edited. New PATCH /crypto/transactions/{id} (partial update, never calls
CoinGecko, same oversell guard as creating a sell) backs a pencil button on
each history row that opens CryptoTransactionModal pre-filled — the same
dialog used for "+", now doing double duty as create/edit.

86/86 backend tests passing (4 new). Verified live against the real API:
create, edit quantity/note, 404 on unknown id, cleanup — real user data
untouched throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 05:06:20 +03:00
ZProger
dc9421e08a fix: Best/Worst Performer compared personal ROI, not coins against each other
best/worst were picked by profit_loss_percent (return since the user's own
purchase price), so a coin bought cheap long ago could show as "best"
without actually being the strongest mover right now. Switched to
price_change_24h — the same market-price move already shown in the table's
24h% column — so the comparison is coins against each other by how they're
actually performing today, independent of when/at what price each was
bought. 24h was the user's explicit choice over 1h/7d.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 04:59:35 +03:00
ZProger
19d8aeda3a fix: allocation view as a real table (color · ticker, %, amount)
Replaces the flex-based list (coin logo + full name + ticker suffix,
prone to truncation and misaligned columns) with an actual <table>: a
color swatch + ticker, percent, and amount as three real columns that
align natively instead of fighting flexbox for it. Full coin name is
still available via a title tooltip.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 04:52:33 +03:00
ZProger
7dd9c83457 fix: allocation list stretched full-width with few coins
The coin list next to the donut was flex-1, so with only one or two
holdings it stretched across whatever space was left in the card and the
percent/amount columns ended up stranded far from the coin's icon and name.
Capped the list at a fixed width and centered the donut+list group instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 04:49:04 +03:00
ZProger
7a23ed1ff8 feat: donut allocation chart + History/Allocation tabs on Crypto tab
Merges the separate history chart and allocation cards into one
CryptoOverviewCard with History/Allocation tab pills, matching CoinMarketCap's
layout — switching views instead of stacking two cards keeps the tab's
footprint on the page instead of doubling it. The total value, eye toggle,
and change-over-range line stay in the shared header regardless of which
tab is active.

Allocation is now a donut (Recharts Pie, innerRadius 68%, paddingAngle gap
between segments per the dataviz skill's mark spec) instead of a plain bar.
Unlike CoinMarketCap's own version, the list next to it shows both percent
and the $ amount per coin, per explicit request — CMC's reference only
shows percent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 04:47:13 +03:00
ZProger
5f34d0a612 fix: "hide balance" on Crypto tab only masked the chart's header number
The eye toggle lived as local state inside CryptoHistoryChart, so it only
masked the big total figure — the holdings table, allocation card, and
stats tiles below stayed fully visible. Lifted `hidden` to CryptoPage and
threaded it through every money-displaying section (chart, stats row,
allocation list, holdings table, trade history modal) via a shared
maskAmount() helper.

Percentages (1h/24h/7d%, profit/loss %) stay visible on purpose — a
relative change doesn't reveal the actual amount of money involved, only
absolute currency figures and coin quantities (which combined with a
visible price also reveal an amount) are masked.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 04:43:30 +03:00
ZProger
4ea83a9cda feat: crypto portfolio analytics (history chart, allocation, P&L, cost basis)
Adds the analytics half of the CoinMarketCap reference: a value-over-time
chart (GET /crypto/history, forward-filled from AssetValuation like Net
Worth's own series but scoped to crypto assets), an allocation breakdown by
coin, and stat tiles for all-time profit, total invested (cost basis), and
best/worst performer.

Deliberately no "24h" range — history is only as dense as the sync cadence
(button or once/day), so a 24h chart would be one or two points, not a
smooth intraday line. Cost basis / all-time profit / best-worst performer
aren't separate endpoints: they're trivial to derive client-side from
fields already in GET /crypto/holdings, so no redundant backend logic.

Allocation card follows the dataviz skill's categorical rule: top 7 coins
by value get a fixed-order color from the app's already-validated 8-slot
ramp, an 8th+ folds into "Other" rather than generating a new hue.

82/82 backend tests passing (3 new for /crypto/history). Verified live
against the real CoinGecko API — and caught a real holding the user had
already added directly through the UI between messages, which the smoke
test correctly left untouched while cleaning up only its own test data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 04:38:51 +03:00
ZProger
e8cd536584 feat: full CoinMarketCap-style crypto portfolio (buy/sell log, 1h/24h/7d%, P&L)
Replaces the plain quantity field with a CryptoTransaction log (same
"record events, derive the total" shape as Goal/GoalContribution): quantity
and average buy price are now computed via weighted-average cost — a buy
blends into the running average, a sell reduces quantity but leaves the
average cost of what's still held unchanged.

Switches price fetching from /simple/price to CoinGecko's /coins/markets
(still one batched call for every coin), which also returns 1h/24h/7d %
change in the same request. Old PATCH /crypto/holdings/{id} is replaced by
POST .../transactions (buy/sell, rejects selling more than held), GET
.../transactions (history), and DELETE /crypto/transactions/{id}.

Frontend: CryptoHoldingsTable replaces the simple list with a real table
(Name/Price/1h%/24h%/7d%/Holdings/Avg Buy Price/Profit-Loss/Actions),
horizontally scrollable on mobile rather than dropping columns. New
CryptoTransactionModal (buy/sell) and CryptoTransactionHistoryModal.

Backend: 79/79 tests passing (13 in test_crypto.py, up from 9). Also
verified live end-to-end against the real CoinGecko API with the user's own
key — create, buy, sell, oversell rejection, history, manual refresh, all
against real BTC/ETH prices in EUR — test data cleaned up after.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 04:25:03 +03:00
ZProger
7e4508d1de feat: add a Crypto tab with live CoinGecko prices synced into Net Worth
Track individual coins (identity + quantity) in a dedicated tab, backed by
CoinGecko's Demo API (AURUM_COINGECKO_API_KEY). A holding is just another
Asset under the hood (asset_class=crypto) with its value written as a
regular AssetValuation — the existing Net Worth aggregation needs zero
changes to pick it up.

Prices never refresh in the background (no scheduler in this stack, same
reasoning as recurring transactions): a "Refresh prices" button forces a
sync, and GET /crypto/holdings lazily re-syncs at most once every 24h.
CoinGecko outages leave existing values untouched (error_key: "unreachable")
instead of failing the request. Backup/restore now carries CryptoHolding
rows too, so a restore doesn't silently strip a holding's coin identity.

Backend: 75/75 tests passing (9 new), including a real identity-map
staleness bug the tests caught in the raw-Core-upsert + ORM re-read pattern,
fixed via populate_existing=True.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 03:58:04 +03:00
ZProger
f74305f0be feat: add "remember me for 30 days" to the login screen
Default stays sessionStorage (signed out on tab close). Checking the box
instead stores the credential in localStorage with an explicit 30-day
expiry checked on every page load, so staying logged in has a hard ceiling
rather than lasting forever.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 03:10:47 +03:00
ZProger
1de9ac272e feat: branded login screen in front of Basic Auth
The browser's native Basic Auth prompt can't be restyled — it's chrome UI,
not part of the page. Replace it with Aurum's own login screen: nginx keeps
auth_basic on /api/* (the actual protection, unchanged), but drops it from
the SPA shell (just HTML/JS/CSS, no data) so the app can render its own
branded form instead. On submit the app validates the credentials against a
real protected endpoint, then attaches the resulting Authorization header to
every request itself — sessionStorage-backed, so closing the tab signs out.
Instances with Basic Auth left off (the default) never see this screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 03:05:01 +03:00