Commit Graph

63 Commits

Author SHA1 Message Date
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
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
technosexual
76c5caa667 fix: actually reject zero-amount goal contributions (#4)
`GoalContributionCreate.amount` used `Field(ne=0)`, but pydantic has no
"not equal" constraint — the keyword was accepted as unknown schema
metadata and never validated anything. `POST /api/goals/{id}/contributions`
with `"amount": "0"` returned 201 and wrote a contribution that moves the
goal's progress by nothing, and the comment right above the field says zero
is pointless.

Replaced with a field_validator so the rule is enforced, deliberately not
with `gt=0`: negative contributions are intentional (money taken back out of
a goal), and the added tests pin both halves of that.

This also clears the `PydanticDeprecatedSince20: Using extra keyword
arguments on Field (Extra keys: 'ne')` warning that the test suite printed
on every run — that warning was the symptom.


Claude-Session: https://claude.ai/code/session_01UmuMDFrwdvQkce885VaaTh

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 02:43:11 +03:00
technosexual
949d2df37f fix: enforce transfer invariants when updating a transaction (#3)
The transfer rules — destination required, destination different from the
source account, no category, and no destination on a non-transfer — were
enforced by TransactionBase's validator on create, but the PATCH route only
re-checked category-vs-type.

So `PATCH /api/transactions/{id} {"type": "transfer"}` on an ordinary
expense committed a transfer row with transfer_account_id = NULL, and only
then failed serializing the response (TransactionRead inherits the same
validator). The caller got a 500 and would reasonably assume nothing was
saved — but from that point on every `GET /api/transactions` failed the same
validation, so the transactions list, the app's main screen, returned 500
permanently, with no way left in the UI to delete the offending row.

The rules now live in one place, transfer_rule_violation(), used by both the
create schema and the update route, so the two paths can't drift apart
again. The update path validates the row as it would look after the patch
rather than only the fields sent, since switching type alone invalidates
fields that were left untouched.

The frontend already sends a complete payload on edit (explicit nulls for
category_id / transfer_account_id) and does these same checks client-side,
so this only closes the hole for direct API calls — no UI change needed.


Claude-Session: https://claude.ai/code/session_01UmuMDFrwdvQkce885VaaTh

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 02:42:37 +03:00
ZProger
c95eb12d57 docs: add full REST API reference (DOCS.md)
Aurum already exposes the same REST API its frontend uses; this documents
every endpoint (accounts, categories, tags, transactions incl. bulk CSV
import, recurring, budgets, goals, assets/net worth, dashboard/cash-flow/
reports, insights/advice, settings, backup) with request/response shapes,
enforced business rules, and curl recipes, so the API can be published and
used to integrate other programs with Aurum. README links to it from the
top nav and a new API section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01855PiW5Wd8n4aRW4n9kByY
2026-08-30 02:28:22 +03:00
ZProger
13d5d4833a docs: sync README with CSV import, tags, subcategories, idle cash alert, theme toggle
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FfUH7LVvXrsBTyGiVFHmGL
2026-08-25 19:36:32 +03:00
ZProger
19f5cfa429 feat: import transactions from a CSV bank statement
Fourth of the four ideas picked from the Maybe/Sure feature audit, and the
last one queued up. A 3-step wizard (account -> column mapping -> preview)
at /transactions/import, parsed entirely client-side (new lib/csv.ts —
delimiter auto-detect, 5 date formats, thousands/decimal-separator-aware
amounts) and submitted through a new all-or-nothing POST /transactions/bulk
endpoint, mirroring backup restore's validate-everything-before-touching-
anything approach.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 02:44:30 +03:00
ZProger
94782ee073 feat: add free-form tags on transactions
Third of the four ideas picked from the Maybe/Sure feature audit. Tags are
orthogonal to categories (category = what kind of spend, tag = which
event/project) — many-to-many, created inline while typing in the
transaction form, filterable from the Transactions page.

Also adds tests/test_backup.py, the first test coverage the backup
export/import path has ever had, and fixes a MissingGreenlet bug it
surfaced in restore_backup's tag-wiring.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 02:38:04 +03:00
ZProger
09a0e383e0 feat: idle cash alert and category hierarchy (subcategories)
Two of the four ideas picked from the Maybe/Sure feature audit:

- Idle cash: a 5th proactive alert flags a depository account (checking/
  savings/cash) sitting at or above a configurable balance with no
  transaction touching it for a configurable number of days.
- Category hierarchy: categories can now have one level of subcategories
  (Groceries -> Alcohol). Dashboard/report aggregations roll a
  subcategory's spending into its parent's slice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 02:27:46 +03:00
ZProger
79e4ebb06b fix: sort category lists alphabetically on the Categories page
Expense/income lists were rendered in server sort_order (chart palette
slot order), which looks arbitrary for browsing/managing categories.
Sorts by translated name instead, locale-aware — same approach
BudgetFormModal's category picker already uses. Display-only; sort_order
itself is untouched since the dashboard donut chart still relies on it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 23:56:54 +03:00
ZProger
2c6fcd628c feat: add 4 default income categories, localize category display names
Fresh installs (this app is published on GitHub) only got Salary,
Freelance, Investments, Gifts, Other Income by default — adds Business
Income, Rental Income, Benefits, and Item Sales, rounding out the 8-slot
categorical palette. Only affects new databases (seeding is a one-time
no-op once any category exists).

Also fixes CategoryList.tsx (added last commit) to run category names
through translateCategoryName() like every other category display in the
app, and registers the new defaults' translation keys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 23:51:33 +03:00
ZProger
9f5ea00227 feat: add category management page (create/edit/delete)
The backend already had full category CRUD but nothing in the UI exposed
it — categories could only be viewed inside charts. Adds a Categories nav
item and page with icon/color pickers, grouped by expense/income, backed
by new create/update/delete mutations. Also expands icons.tsx's ICON_MAP,
which was missing 14 icon keys already in use by real category data and
silently falling back to a wallet glyph everywhere they render.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 23:43:06 +03:00
ZProger
6cc532378b feat: auto-capitalize the first letter of transaction descriptions
Transactions/recurring templates entered with mixed casing ("траты на
продукты") are now capitalized on create/update via a Pydantic validator,
so the fix applies to every client, not just the web UI. Adds a one-off
backfill script for existing rows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 23:10:22 +03:00
ZProger
2b2e01a8e2 feat: add search to Transactions tab, spanning all history
Lets the user find a past purchase by keyword instead of paging through
months manually — search hits description/merchant/notes and ignores the
month filter, with a "jump to month" action on each result to view it in
its normal monthly context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-20 23:00:44 +03:00
ZProger
b9ff3ab404 fix: Transactions открывался на январе вместо текущего месяца
parseMonthParam передавал Number(null) дальше — а это 0, не NaN, поэтому
проверка "отсутствует/невалиден -> fallback" не срабатывала и месяц
зажимался в 1. Добавил явную проверку value === null и диапазон 1-12
вместо клампинга (тот же фикс для parseYearParam для единообразия).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 02:39:17 +03:00
ZProger
bbbd67bc72 fix: risk_level актива не сохранялся при создании и не отдавался в API
Asset(...) в create_asset игнорировал payload.risk_level, а _to_read()
не передавал risk_level в AssetRead — из-за дефолта в схеме API всегда
отвечал Medium вне зависимости от выбора в форме и данных в БД.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 02:14:09 +03:00
ZProger
d1458eb160 feat: view and edit a transaction's note from the list
Transactions can carry a `notes` field (set via the API — e.g. the
currency-conversion metadata on this user's imported historical data), but
there was no way to see it in the UI at all.

TransactionsTable now shows a note icon next to edit/delete, only when a
transaction actually has one, opening a dialog with the text. The edit
form's `notes` field was already wired end to end (state, payload) but had
no input in the JSX — added it, since a view-only note with no way to edit
or clear it would be a dead end.

Also fixed e2e/run.sh's readiness check, found while running the full
suite for this change: it polled `/` (nginx, answers almost immediately)
instead of `/api/health`, so the first test could start before the backend
finished migrating/seeding and fail trying to parse nginx's HTML error
page as JSON.
2026-08-16 23:50:10 +03:00
ZProger
7235d74f80 fix: move category spending chart above the ranking list on Reports
Picking a category at the top meant scrolling past the whole "top
categories" list before reaching its chart, then scrolling back up to
pick another one. Swapped the two so the chart renders immediately after
the category/range selector.
2026-08-16 23:37:51 +03:00
ZProger
1f5fab5010 refactor: merge Language and Theme settings into one two-column card
Language and Theme were two separate Cards stacked on top of each other —
both are pure client-side display preferences, so splitting them side by
side (language left, theme right) saves a card's worth of vertical space.
Collapses back to a single column on mobile.
2026-08-16 23:27:55 +03:00
ZProger
75dd43c8e3 feat: manual theme picker (light/dark/system) in Settings
Theme previously followed only the OS's prefers-color-scheme, which
switches automatically on this user's Mac at sunset/sunrise. Adds an
explicit override in Settings: Light/Dark/System, System being the
previous (default) auto-follow-the-OS behavior.

Persisted client-side in localStorage (lib/theme.ts, same pattern as
language) rather than server-side like currency — it's a pure display
preference, and synchronous localStorage reads are what make the
anti-FOUC inline script in index.html possible. index.css already had the
[data-theme] CSS variables laid in ahead of time; only the JS to actually
set the attribute, and the Settings UI, were missing.

Covered by 2 new Playwright specs (theme-switcher.spec.ts) run against the
isolated e2e stack: explicit choice overrides a dark OS and survives
reload; switching back to "system" doesn't leave a stale forced theme.
2026-08-16 23:12:20 +03:00