Commit Graph

83 Commits

Author SHA1 Message Date
dependabot[bot]
c8bab36f09 build(deps): bump the actions group with 3 updates
Bumps the actions group with 3 updates: [actions/checkout](https://github.com/actions/checkout), [actions/setup-python](https://github.com/actions/setup-python) and [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/checkout` from 4 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

Updates `actions/setup-python` from 5 to 7
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v7)

Updates `actions/setup-node` from 4 to 7
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-08 22:16:22 +00:00
ZProger
4cb5c4788a ci: группировать Dependabot-обновления по minor/patch и major
Все minor/patch-апдейты одного пакетного менеджера теперь идут одним
PR, а major-обновления — отдельным, чтобы не терялись среди рутинных
и не сыпало десятком PR одновременно.
2026-09-09 01:15:33 +03:00
ZProger
2d4f405167 ci: включить Dependabot для npm, pip и GitHub Actions
Еженедельные PR на уязвимые/устаревшие зависимости во frontend,
backend и в самих CI-экшенах, без ручного отслеживания.
2026-09-09 01:05:26 +03:00
ZProger
e5a474fb38 test: покрыть risk_level и network на крипто-холдингах
Создание с дефолтным/кастомным risk_level, правка через PATCH
/assets/{id}, создание/правка/сброс network через PATCH
/crypto/holdings/{id}, partial-update и 404 на неизвестный id.
2026-09-09 01:04:52 +03:00
ZProger
cc4a78dd30 docs: сослаться на PR-шаблон из CONTRIBUTING.md 2026-09-09 01:00:48 +03:00
ZProger
a42a19b9ce docs: добавить шаблон PR
Ссылается на чек-лист из CONTRIBUTING.md вместо дублирования, плюс
пункт про реальную проверку (не просто "тесты прошли" на словах) и
про отсутствие личных данных/ключей в дифе.
2026-09-09 00:59:27 +03: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
ZProger
e4b163df21 docs: упростить требование по коммитам, убрать UPDATES.md 2026-09-08 20:32:14 +03:00
ZProger
2fa61005b5 docs: добавить пример вызова API из чат-бота (Telegram/n8n) 2026-09-08 20:30:11 +03:00
whaley
12c3a7d79c add "debit_card" as a new AccountType (#16) 2026-09-08 20:03:13 +03:00
ZProger
ae89311a8e fix: чинит тесты 90d-доходности, падавшие в CI без CoinGecko-ключа 2026-09-04 03:06:59 +03:00
ZProger
9c98f5df96 fix: seed app_settings.currency от AURUM_DEFAULT_CURRENCY на чистой установке 2026-09-04 03:03:29 +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
b9c7ecca08 test: verify direct push to main is blocked (will be discarded) 2026-09-02 02:49:02 +03:00
ZProger
01426ea9eb ci: добавить GitHub Actions для тестов бэкенда и сборки фронтенда
Раньше пул-реквесты никак не проверялись автоматически — сломанный PR
можно было смержить, ничего бы не показало проблему. Теперь на каждый PR
и пуш в main гоняются 115 backend-тестов (реальный Postgres в сервис-
контейнере, та же схема, что и в tests/README.md) и сборка фронтенда
(tsc + vite build).
2026-09-02 02:35:45 +03:00
ZProger
4a12cfb371 fix: заменить битые ссылки на GitHub Discussions ссылками на Issues
Discussions в репозитории не включены (has_discussions: false), поэтому
../../discussions отдавал 404 в README и CONTRIBUTING. Issues уже
включены и используются для багов — направляем туда же и предложения фич/
неочевидные случаи лицензии.
2026-09-02 02:29:27 +03:00
ZProger
4aaa0a0ef0 docs: sync README and DOCS with health version, category delete rules, split transactions
/api/health now returns a version field, default categories can be deleted
once unused, and split transactions (dividing one purchase across a parent
category's subcategories) were fully undocumented — added a dedicated
section in DOCS.md and a mention in README's feature list. Also fixed the
"update to newer code" command, which pulled a Docker image tag that
doesn't exist for locally-built services instead of the actual git repo.
2026-09-02 02:26: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
1e9506470f fix: /api/docs возвращал 404 вместо Swagger UI
FastAPI регистрировал встроенные роуты документации на дефолтных путях
(/docs, /openapi.json), в то время как все остальные роуты подключены с
префиксом /api и только его проксирует nginx. Теперь docs_url/redoc_url/
openapi_url явно указаны под /api, поэтому Swagger UI доступен там же,
где его и ищут (как заявлено в README).
2026-09-02 02:01:15 +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
f778742134 docs: update project instructions in CLAUDE.md 2026-08-30 18:43:26 +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
technosexual
b045d378f0 fix(security): stop granting every origin credentialed access to the API (#5)
`AURUM_CORS_ORIGINS` defaults to `*`, and the middleware was configured with
`allow_credentials=True`. Starlette does not answer that combination with a
literal `*` — it echoes back whatever `Origin` the caller sent and adds
`Access-Control-Allow-Credentials: true`, so any page open in the same
browser can call the instance, with the user's Basic Auth attached, and read
the response:

    GET /api/accounts   Origin: https://evil.example
    -> Access-Control-Allow-Origin: https://evil.example
       Access-Control-Allow-Credentials: true

That undercuts the README's advice that leaving Basic Auth off is fine while
the instance is only reachable from localhost: reachable-from-localhost
includes every website the user visits.

Nothing in the shipped setup is cross-origin. Compose serves the UI and the
API from the same nginx, and the Vite dev server proxies `/api`
(frontend/vite.config.ts), so neither deployment nor development needs CORS
at all. The default is therefore empty — no cross-origin access — and the
middleware is only installed when origins are configured deliberately.

An explicit `*` is still honoured for a genuinely public read-only setup, but
never together with credentials, which is what the spec intends `*` to mean
anyway.
2026-08-30 18:11:09 +03:00
technosexual
3013466c05 fix(budgets): count subcategory spending against the parent's budget (#6)
get_budget_status() summed only transactions whose category_id matched the
budgeted category exactly, while the Dashboard breakdown and the category
report both roll a subcategory's spending up into its parent
(coalesce(parent_id, id) in dashboard_service.py and reports_service.py).

With one level of subcategories — the app supports exactly one — a budget on
"Housing & Utilities" ignored everything filed under "Rent": the Dashboard
showed 900 spent for the month and the Budget page showed 0 of 1000 for the
same category, same month. Since insights_service's over-budget alert reads
this same status, the alert stayed silent too.

Budgets now count the budgeted category plus its children, matching the
rollup the other two screens already use. A subcategory that has a budget of
its own keeps tracking its own spending, so both bars move against their own
limits — that case is pinned by a test as well.
2026-08-30 18:10:53 +03:00
technosexual
20f41b444a fix: don't enforce write-time transfer rules when reading transactions (#7)
TransactionRead inherited TransactionBase's model_validator, so the rules a
new row must satisfy were also applied on the way out, to rows already in
the database. A row can stop satisfying them through no fault of its own:
transactions.transfer_account_id is ON DELETE SET NULL, so deleting an
account leaves every transfer that pointed at it without a destination.

The result was that deleting an account could 500 the entire transactions
list — permanently, and with the list gone there was no way in the UI to
find and delete the offending row.

The fields move to TransactionFields, which reading uses; TransactionBase
keeps the invariants for the write path. The frontend already tolerates a
missing destination (TransactionsTable renders the transfer suffix only when
transfer_account_id is set, and the edit modal makes you pick a destination
before saving), so such a row is now visible, editable and deletable
instead of fatal.
2026-08-30 18:10:23 +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