Files
rmcampos 65f6985278
Frontend CI / build (push) Successful in 2m9s
Backend CI / build-and-push (push) Successful in 7m22s
feat: add usd brl exchange rate request
2026-07-12 18:09:38 -03:00

61 KiB
Raw Permalink Blame History

Ledger — Outstanding Work

Status as of end of session (2026-07-11, latest session, continued)

Done, not yet committed: bill-offset support for repeat/installment series on Card Bills. Bug found via user testing right after the §8 work above: on CardBills.jsx's add-transaction form, picking any Repeat value other than "Doesn't repeat" made the Bill dropdown disappear entirely — pre-existing, deliberate restriction from the original credit-card work (billDueDate was only ever applied when repeat == NONE, both frontend and backend), but it blocked a real use case: a purchase made before the due day but after the bank's statement already closed needs to land on next month's bill instead of the natural one — same logic that already worked for one-off transactions, just never extended to series.

Fixed by having the override apply as an offset: the user's chosen bill for the first occurrence is compared to that occurrence's natural bill to derive a whole-month offset (e.g. picking "next bill" = +1), then every generated occurrence's own natural bill is shifted by that same offset — so a MONTHLY/INSTALLMENTS/etc. series starting on an overridden bill keeps each subsequent charge one bill cycle ahead of where it'd naturally land, rather than only the first occurrence being correct.

  • Backend: CreditCardBillSyncService#nextDueDate (the Java port of the due-day roll-forward math) changed from private to package-private so TransactionResource can reuse it. New TransactionResource#billOffsetMonths (natural-vs-chosen bill → whole-month offset, null when no genuine override) and #shiftedBillDueDate (an occurrence's own natural bill, shifted by that offset, clamped for short months). Wired into both create()'s batch-generation loop (replacing the old repeat == NONE ? request.billDueDate : null line) and applyToSeries() (replacing a flat t.billDueDate = request.billDueDate that would have pinned every future-scoped row to one identical date instead of shifting each by the same offset — found and fixed in the same pass, confirmed with user it should cover both create and edit).
  • Frontend (CardBills.jsx): the Bill dropdown (relabeled "First bill" when a repeat is selected) now always renders regardless of repeat, laid out on its own row when Occurrences is also showing; isOverride in handleSubmit no longer gates on repeat === 'NONE'. The edit form's bill override was already ungated (single-row edits never had this restriction) — no change needed there.

Verified via curl (fresh signup, CREDIT_CARD account with dueDayOfMonth 10): a MONTHLY series of 3 starting 2026-01-05 (natural bill 01-10) with an explicit billDueDate=2026-02-10 override correctly produced bills 02-10, 03-10, 04-10 across the three occurrences (each exactly one cycle ahead of its own natural bill); an INSTALLMENTS series with the same inputs matched identically; a 4-occurrence MONTHLY series created with no override (all billDueDate null, natural fallback) then edited at occurrence 2 with scope=FUTURE and a one-bill-ahead override correctly left occurrence 1 untouched (null) and shifted occurrences 2-4 to 03-10/04-10/05-10 respectively — each by the same offset, not one flat date. Also verified via Playwright: the Bill dropdown (as "First bill") and Occurrences field render together for a MONTHLY selection, with the new explanatory hint text visible, no console errors.

Status as of end of session (2026-07-11, earlier in the session)

Done, not yet committed: §8 series tracking + scope-aware edit/delete (requested 2026-07-11, planned via plan mode, plan saved at /home/ricardo/.claude/plans/idempotent-hugging-cupcake.md). Resolves §8's long-standing gap (repeat/installment rows only shared a cosmetic seriesInfo string, no real grouping key) and adds the "apply to future occurrences?" prompt on edit/delete the user asked for.

  • Backend: two new nullable columns on TransactionseriesId (Java UUID.randomUUID().toString(), VARCHAR(36), V8__transaction_series_id.sql, indexed) and seriesRepeat (the RepeatFrequency the batch was created with, V9__transaction_series_repeat.sql) — both set once per batch in TransactionResource#create, identically across every generated row, both null for one-off transactions (no backfill for pre-existing rows — they keep their seriesInfo display string and fall back to plain single-row edit/delete, per §8's already-accepted tradeoff).
  • New EditScope enum (THIS/FUTURE). PUT /transactions/{id} gained an optional scope body field (default THIS, backward compatible); DELETE /transactions/{id} gained an optional ?scope= query param (default THIS). FUTURE is only honored when the target row has a non-null seriesId.
    • Edit "this and future": description/category/billDueDate flat-set across every row from the edited occurrence onward (via new seriesFrom(anchor), occurredOn-then-id ordered). occurredOn is never propagated — only the anchor row's own date changes. amount on an INSTALLMENTS series re-splits the entered total evenly across the remaining rows (reuses the existing splitAmount helper, same remainder-on-last-row rule as creation); non-INSTALLMENTS series flat-set every affected row to the entered amount.
    • Delete "this and future": deletes every row from the target occurrence onward in the same series.
    • Every delete (single or batch) calls new renumberSeries(), which relabels remaining rows' seriesInfo contiguously (closing gaps like 1/12,2/12,4/12,5/121/11,2/11,3/11...), and clears seriesId/seriesInfo/seriesRepeat entirely if a series collapses to one remaining row (it silently reverts to a plain non-series transaction — no scope UI shown for it anymore).
  • Frontend (Transactions.jsx and its line-for-line duplicate block in CardBills.jsx, both updated identically): the in-row edit form shows a "This occurrence only" / "This and future" radio pair — and, when the latter is picked on an INSTALLMENTS row, a one-line re-split hint — only when t.seriesId is set; the delete confirm panel swaps its single "Delete" button for "Delete this only" / "Delete this & future" under the same condition. Non-series transactions render zero new UI, byte-for-byte unchanged from before. TransactionsApi.remove gained a defaulted scope param ('THIS'); update's payload just gained one more optional key.

Verified via curl (fresh signup, INSTALLMENTS batch creation confirms shared seriesId; PUT .../2?scope=FUTURE on occurrence 2 of 4 re-split a new $90 total across occurrences 2-4 leaving occurrence 1 untouched, and only occurrence 2's own date changed; DELETE .../1?scope=THIS renumbered the remaining three from 2/4,3/4,4/4 to 1/3,2/3,3/3; a further DELETE ...?scope=FUTURE collapsing a series to one row correctly nulled out its seriesId/seriesInfo/seriesRepeat; a separate uneven-total re-split ($100 over 3) landed 33.33/33.33/33.34, remainder on the last row as designed; a plain non-series transaction created/deleted with no scope param at all, confirming backward compatibility) and via a fresh Playwright pass (playwright-core + cached Chromium installed ad hoc into scratchpad, no OS-level deps available so --with-deps wasn't used) on both Transactions.jsx and CardBills.jsx: scope radios and the INSTALLMENTS hint appear only on series rows, the re-split renders correctly in the row list after saving, deleting "this only" renumbers the badge in place, and a non-series row (Coffee) shows the exact pre-change single-button edit/delete UI with no radios. No console errors in any pass.

Status as of end of session (2026-07-09, previous session)

Product.md audit → 5 fix chunks, all done, each reviewed/committed individually by the user as we went. User wrote Product.md (repo root, new file) spelling out expected behavior for Overview/Accounts/Credit Cards. Three subagents audited the actual code against it; findings below, in the order fixed:

  1. Overview: "Checking & Savings accounts" list was kind !== 'CREDIT_CARD' (leaked INVESTMENT accounts in) instead of reusing liquidAccounts. Also added a new "Investments" section and a third hero-panel column ("Total investments") per user follow-up request — same today-cutoff balance logic as the other two columns.
  2. Backend: added Transaction.createdAt (V5 migration, Instant, @PrePersist-guaranteed on every insert path incl. repeat/installment batches and CreditCardBillSyncService's system rows) — needed for #3. columnDefinition carries the DEFAULT now() into Hibernate's dev-mode DDL too, not just the Flyway migration, so seed-data.sql (which doesn't set the column) keeps working in dev.
  3. Overview's "Recent activity" was scoped to the page's date-range dropdown and sorted by occurredOn — spec wants "latest 5 added," global, regardless of account. Now sorts by createdAt desc (id as tiebreaker), ignores range entirely, excludes linkedCard bill-sync rows. This made the date-range dropdown itself dead (it had no other consumer) — removed from Overview's header along with rangeOptions/ rangeBounds/startOfWeek.
  4. Accounts.jsx rendered raw a.balance (all-time, future-inclusive) for every account kind, and credit cards showed that same raw balance instead of a bill total. Now fetches each account's transactions (shares the ['transactions', id] query key with Overview) and shows a proper today-cutoff balance / current-bill amount.
  5. CreditCards.jsx "Available credit" (totalLimit - totalOwed) mixed mismatched sets: totalLimit only summed cards with a limit, but totalOwed summed all cards — a no-limit card's debt silently ate into another card's available credit. Fixed by summing owed only over cardsWithLimit for this one figure (the "Total owed across all cards" stat itself is intentionally unchanged, still all cards). Also "View transactions" now passes an explicit &bill=<dueDate> param (via nextBillFor), matching the pattern Transactions.jsx's "View bill" link already used, instead of relying on CardBills.jsx's implicit default-bill fallback.

Refactor along the way (chunk 4 + a follow-up ask): extracted the "balance as of a cutoff date" walk — previously duplicated near-verbatim in Overview.jsx and Transactions.jsx — into frontend/src/utils/balance.js as two pieces: sortChronologically (sort once) and balanceAsOf(account, sortedTransactions, cutoffDate) (walk many times against the same sorted array) — preserves Transactions.jsx's existing sort-once/walk-per-day-group performance shape instead of resorting on every call. All three pages (Overview, Accounts, Transactions) now call the same implementation. Also promoted a small isoDate formatter (pre-existing local copy in CardBills.jsx) into utils/date.js so CreditCards.jsx's new bill-param link could reuse it instead of a third copy-paste.

Verified via ad hoc Playwright (scratchpad-installed playwright-core, cached Chromium) driving a throwaway signup user (qa-test2@ledger.local) through /auth/signup + direct API calls to seed specific test data (future-dated txn, backdated txn, a limited + a no-limit credit card), then screenshotting/reading Overview, Accounts, Credit Cards, Card Bills, and Transactions. All five fixes confirmed with concrete numbers; no console errors in any pass.

Status as of end of session (2026-07-08, previous session)

Done, not yet committed: split credit cards out of Transactions.jsx onto a new "Card Bills" page. User found the Transactions page overwhelming — a large fraction of it was credit-card-only machinery (bill prev/next navigator, "Projected" section, "Amount owed" sign-flip, the Bill dropdown on add/edit). Landed as two chunks:

  1. New frontend/src/pages/CardBills.jsx (route /card-bills, its own sidebar entry after "Credit Cards"). Auto-selects the user's card (or shows a plain picker if they have more than one), navigates by actual bill due dates via chevrons (reusing groupTransactionsByBill/ nextBillFor from utils/creditCard.js), shows that bill's total + due date, and lists just that bill's transactions with full add/edit/ delete parity including the Bill-period override — no future-grey styling, no other filters. The Credit Cards dashboard's "View transactions" link now points here instead of into Transactions.
  2. Stripped Transactions.jsx from 1244 → 888 lines: credit card accounts filtered out of accounts right after the query (cascades to the account dropdown, txnQueries, every downstream calc), then deleted billOptionsFor/billOptionsWithCurrent, filterBillDueDate, billBalanceAsOf, isCreditCardFilter sign-flip, projectedCcDeduction, aggregateCreditCards/isCreditCardTxn, projectedBills, cardBillDates/defaultBillDueDate/stepBill, viewAccountTransactions, resetToDefaultView, and both Bill dropdowns. Small added robustness: a stale ledger:lastAccountId pointing at a now-filtered-out credit card clears itself back to "All accounts" instead of silently showing an empty list.

Also fixed along the way: utils/date.js#dueLabel read "in -3 days" for an overdue bill (never triggered before, since its only callers passed future dates) — now "3 days ago"/"yesterday". Verified via a scratch Playwright driver (playwright installed ad hoc into the scratchpad dir, reusing the cached Chromium binary) — full add/edit/delete/navigate flow on Card Bills, the dashboard deep link, and a regression pass confirming normal (non-card) Transactions behavior and Overview were unaffected. No console errors in any pass.

Follow-up in the same session: credit card bills sync onto their linked payment account as real transactions. Splitting credit cards out of Transactions.jsx also removed the old client-side "Projected" row, which was the only way a linked checking account showed its upcoming card bill. Rather than rebuild that (a side-channel projection that TODO.md's prior entries show caused repeated double-counting/month-leakage bugs), the user proposed materializing it: whenever a credit card's transactions change, maintain one real Transaction per open bill on the card's paymentAccount, dated on the bill's due date, holding that bill's total.

  • Backend: new nullable Transaction.linkedCard FK (V4__transaction_linked_card.sql) marks a row as system-generated. New CreditCardBillSyncService (com.ledger.anthropic, @ApplicationScoped) owns recomputeAccountBalance (moved out of TransactionResource, same algorithm, now has two callers) and sync(Account card) — the single entry point, safe to call unconditionally on any account. Groups the card's own transactions by effective due date (a Java port of utils/date.js#nextDueDate + utils/creditCard.js#billDueDateFor/groupTransactionsByBill), upserts one row per non-zero bill on the linked account, deletes any synced row whose date no longer matches or that's sitting on the wrong account (this is what makes a payment-account change self-correct with no special-casing). Called from TransactionResource's create/update/ delete and unconditionally at the end of AccountResource#update. TransactionResource rejects direct PUT/DELETE on a transaction with linkedCard != null (400).
  • Frontend: Transactions.jsx renders a linkedCard row dashed, with a "Card bill" tag and a "View bill" link to Card Bills instead of edit/ clone/delete icons. Everything else — amount, running balance, day grouping, today/future styling, CSV export, search/category filtering — needed zero new code, since these are ordinary rows returned by the same GET /transactions/account/{accountId} the page already calls. CardBills.jsx needed no changes (synced rows live on the payment account, never on the card itself).
  • One bug found and fixed during verification: CreditCardBillSyncService #sync set row.amount after row.persist() for a newly-created row — IDENTITY-strategy entities insert immediately on persist() (can't defer for a generated id), so amount (NOT NULL) has to be set first. Fixed by assigning every field before persist().

Verified via the same Playwright driver, fresh throwaway users each time: linked card + checking, add a charge → dashed synced row with correct amount/date and no edit/delete UI; a second charge updates the same row (no duplicate); overriding the Bill dropdown to a different due date moves the amount off the old date onto a new one; deleting the card transaction removes the synced row and recomputes the checking balance; editing the card to a different payment account moves the row (old account ends up empty, new one gets it); direct curl-equivalent PUT/DELETE against a synced transaction's id both return 400. No console or backend errors in any pass.

CLAUDE.md updated: Account.paymentAccount's Javadoc, the "Balance math" bullet's file reference, and a new bullet documenting the sync mechanism and why it replaced the client-side projection.

Follow-up in the same session: "View bill" deep link + a StrictMode race it exposed. The "View bill" link on a synced row only ever opened Card Bills on whatever bill it defaults to (next upcoming, or most recent past) — clicking it from a future-dated bill row still required an extra manual chevron click to actually reach that bill. Fixed by passing the row's own t.occurredOn (already is the bill's due date — no separate bill-id needed) as a new ?bill= param, read by CardBills.jsx alongside the existing ?account=.

First attempt raced: two separate effects both decided what selectedBillDueDate should be, and when the accounts/transactions queries were already cached (e.g. bouncing back from Transactions), both ran in the same pass reading the same stale empty value — whichever ran second (the pre-existing billDates-reconciliation effect) won, silently dropping the URL's bill back to the default. Merging them into one effect "fixed" that pass but broke under React.StrictMode (active in main.jsx), which double-invokes an effect in dev using the same stale closure — a ref mutated in the first invocation made the second invocation skip the "apply the URL bill" branch but still read the stale (pre-update) selectedBillDueDate, falling through to the same reset. Only surfaced with two or more real bills on the card, since with just one, "fallback" and "correct" happened to be the same value — not caught by initial Playwright verification, which always used fresh signups (nothing cached) and single-bill cards.

Root-caused with temporary debug logging read back through the same Playwright driver (page.on('console')), reproducing the exact two-bill scenario. Real fix: decoupled "should the URL's bill be applied" from selectedBillDueDate entirely (removed from the effect's dependency array and its condition) — gated instead on a userSteppedAway ref that stepBill() flips, so the URL-application logic is idempotent no matter how many times React invokes it against stale data. Debug logging removed before commit.

Verified: the two-real-bills repro now lands on the correct bill immediately; the original warm-cache scenario still does too; chevron stepping afterward is unaffected (doesn't fight the URL); no console errors in any pass.

Status as of end of session (2026-07-08, new session)

Done, not yet committed: bill navigation in Transactions.jsx. Follow-up to the credit-card overhaul below, requested in a fresh session. The "Showing {bill} only · View full history" chip was a one-way clear button — once you switched to full history there was no way back to a specific bill, and no way to browse other bills (e.g. the previous month's) at all.

Made it a real toggle, plus prev/next navigation:

  • New groupTransactionsByBill-derived cardBillDates memo — every bill (ascending due date) the currently-filtered credit card has ever had a transaction assigned to, drawn from its complete history (not whatever date range happens to be active), so navigation can reach bills outside the visible window.
  • stepBill('prev' | 'next') moves filterBillDueDate to the adjacent entry in cardBillDates, always switching into an unbounded custom range (same as "View all" already did) so the date filter never hides the target bill. Chevrons disable at either end.
  • The chip itself now toggles both ways: bill-scoped shows "Showing {bill} only · View full history" (click clears to full history); full-history shows "Viewing full history · Show bill" (click jumps to defaultBillDueDate() — the next upcoming bill if it's a real one, otherwise the most recent past bill).
  • billBalanceAsOf (added in the prior session's 8th fix) already made "Amount owed" and day totals follow the bill filter — this now also updates correctly as you step between bills via the chevrons, not just via the initial "View all" click.

Verified via Playwright: a card with 3 bills (July/August/September, one transaction each) — chevrons correctly disable at both ends, "Amount owed" and the day total update at each step, full-history shows the real $90 total across all three, and "Show bill" correctly returns to July (the next upcoming bill relative to today).

Follow-up in the same session: "Back to all transactions" button. The only way out of a bill/account drill-down was clearing the bill filter, which just leaves you on that one card's full history — still not the page's normal default view. Clicking "View full history" required a page reload to get back to browsing normally. Added resetToDefaultView() — clears every filter (account, category, search, range→'this-month', custom dates, bill, month offset) and the remembered-account localStorage entry — wired to a new "← Back to all transactions" button shown whenever the bill navigator is visible (i.e. whenever cardBillDates.length > 0), in both the bill-scoped and full-history states.

Verified via Playwright: from a bill-scoped view, clicking it returns to "All accounts" / "This month" with the normal "Projected" section back, no reload needed.

Follow-up in the same session: flickering $0 "Projected" row. User noticed the projected credit-card bill sometimes showed $0 and sometimes didn't appear at all. Root cause: nextBillFor (in creditCard.js) returns a synthetic $0 placeholder — dated via nextDueDate(dueDayOfMonth, today), not tied to any real transaction — whenever a card has no activity assigned to an upcoming bill yet. projectedBills then filtered that placeholder by whether its date fell in the currently viewed month, so it flickered in and out as today or the viewed month changed, with no visible reason. Fixed by excluding non-positive amounts (p.amountOwed > 0) from projectedBills — the placeholder never had anything to actually project, so it's just dropped rather than shown as a misleading $0 row. nextBillFor itself is untouched (Overview's per-card tile intentionally still shows "$0, due in N days" for a card with no activity — that's a fixed, always-one-row-per-card display with no date-range filtering, so it doesn't have the same flicker problem).

Verified via Playwright: a linked card with zero transactions shows no "Projected" row in June, July, or August (previously flickered depending on the viewed month); adding a real $25 charge makes it appear correctly with the real amount.

Follow-up in the same session: Overview's "Credit card debt" didn't match the per-card "next bill" tiles right below it. User asked why the top total showed $0 while a card's tile showed a real upcoming amount. Root cause: the two figures used genuinely different logic — totalCardDebt summed real debt as of today while excluding anything dated in the future or explicitly tagged outside the current calendar month (from the earlier "should consider only current month" fix), while the per-card tiles use nextBillFor, which shows whichever bill is chronologically next regardless of month or the transaction's date. Confirmed with the user (their call, given the tension with the earlier fix): the total should just be the sum of the tiles, always consistent by construction. totalCardDebt now sums nextBillFor(...).amountOwed per card directly — same function, same result as what's rendered below. This does mean a transaction explicitly deferred to a future bill counts toward the total again when it's that card's only/next open bill (no longer zeroed out for not being "this month's" bill) — that's the accepted tradeoff for the total always matching what's on screen.

Verified via Playwright: a card whose only transaction is a $80 charge explicitly tagged to next month's bill now shows "$80.00" in both the top total and its own tile (previously $0 vs $80 — the reported mismatch).

Follow-up in the same session: Transactions page filter-card redesign. User found the filters card (search, account, category, range, Export all crammed into one panel plus a second row of month/date controls) cluttered and confusing. Restructured in Transactions.jsx:

  • New toolbar row directly below the page header, above the balance panel: Account select + range mode select ("This month"/"Custom range")
    • either month chevrons/label or FromTo date inputs inline, depending on mode — the two most important filters, always visible, no longer buried in a bordered card.
  • Search + Category moved into a collapsible "More filters" panel (new showMoreFilters state), collapsed by default; shows a small "Active" tag next to the toggle when collapsed with a search term or category selected, so an active filter is never silently hidden.
  • Export demoted to a small ghost button on the same low-emphasis row as the "More filters" toggle, right-aligned — no longer competing visually with the primary filters.
  • All existing behavior preserved as-is: filterBillDueDate still clears on every account/range/month/date-range change, the credit-card bill navigator (chevrons + toggle + "Back to all transactions") still lives in the balance panel and works unchanged, and the "your real balance — not limited to..." disclaimer still appears correctly.

Verified via Playwright: default view shows the compact toolbar with "More filters" collapsed and Export tucked to the side; toggling shows search+category and the "Active" chip appears correctly when collapsed with a filter set; switching to "Custom range" swaps in date inputs inline; the credit-card bill navigator and new toolbar coexist correctly (tested filtering to a card, stepping through "View all").

Follow-up polish in the same session: the range select still showed the static label "This month" next to a separate "Jul 2026" span flanked by chevrons — two elements doing one job. Merged them: the select's "this-month" option now renders the live month label itself (e.g. "Jul 2026"), and the prev/next chevrons wrap the select directly instead of a separate label — so it reads as one control, < [Jul 2026 ▾] >, that's also still a real dropdown (selecting "Custom range" from it works exactly as before). No behavior change, purely visual consolidation.

Status as of end of session (2026-07-08)

Done, not yet committed: Credit card overhaul (requested 2026-07-08 via Credit-Card-Issues.md, planned via plan mode, plan saved at /home/ricardo/.claude/plans/woolly-skipping-quasar.md). Landed as 4 chunks, each independently reviewable/committable:

  1. Transaction.billDueDate (backend) — new nullable LocalDate column (V2__transaction_bill_due_date.sql), settable/clearable via CreateTransactionRequest/UpdateTransactionRequest. Display/grouping tag only, same spirit as seriesInfo — never read by recomputeAccountBalance. For repeat/installment transactions the override is only applied to the batch's semantics correctly: every generated row gets billDueDate = null regardless of what was sent (a fixed date copied onto every installment would be wrong), verified via curl.
  2. Bill dropdown + shared util (frontend) — new frontend/src/utils/creditCard.js (billDueDateFor, groupTransactionsByBill, nextBillFor), the single source of truth for "what bill is this transaction in" / "what's the next bill", replacing the old computed-only grouping in Transactions.jsx. Add/edit transaction forms show a "Bill" dropdown (3 upcoming due dates) when the account is a credit card with dueDayOfMonth set — solves "a purchase made after the statement effectively closes should land on next month's bill, not this month's." Only shown for non-repeating transactions (matches the backend scope limit). Verified end-to-end in browser: overriding to "Aug 10 bill" correctly moved the "Credit card bills" aggregate from Jul 10 to Aug 10.
  3. Overview redesign — Total Balance now sums only CHECKING/SAVINGS (excludes INVESTMENT and CREDIT_CARD, confirmed with user); a second figure shows total credit-card debt. Credit card accounts get their own "Credit cards" section below the regular accounts grid, showing each card's next bill (amount + due date), not just current balance.
  4. Account.paymentAccount + projected bill row — CREDIT_CARD accounts can link a CHECKING/SAVINGS "payment account" (V3__account_payment_account.sql, validated in AccountResource#validatePaymentAccount: must be CREDIT_CARD-only, can't self-reference, target must be CHECKING/SAVINGS owned by the same user). In Transactions, viewing that linked checking account (or "All accounts") now shows a dashed, read-only "Projected" row — "Credit card bill — {card name}" — for the card's next bill. It's not a real Transaction (no id, no edit/delete), and only affects the projected/ future balance stat (via a new projectedCcDeduction applied solely to futureBalanceRaw), never the real currentBalanceTotal/ balanceAsOfDay. Verified in browser: Checking's real "Current balance" stayed $0 while its July-end projected balance correctly showed -$75.50 matching the pending card bill.

Verified via curl (backend validation: 400s for non-CC paymentAccountId, CREDIT_CARD/INVESTMENT target, self-reference) and via a full Playwright browser smoke test (signup → create linked checking+card → add transaction with bill override → check Overview/Transactions render correctly), no console errors. No automated test suite exists in this repo, so this was the verification path (same as prior chunks).

Follow-up fixes found via user testing (2026-07-08, same session), both in Transactions.jsx:

  1. Bill leakage across months. creditCardBillGroups ("Credit card bills" aggregate) grouped by each transaction's assigned bill due date, but never checked that due date actually fell within the currently viewed month — so a transaction dated this month but tagged (via the new Bill dropdown, or a natural due-day rollover) to next month's bill would "leak" that future due date into the current month's view, confusingly. Pre-existing behavior before this session's work, but the new per-transaction Bill override made it much easier to trigger. Fixed by having isAggregatedCreditCardTxn also check the assigned bill's due date against the active date-range filter — when a transaction's bill falls outside the viewed range, it now falls through to the normal per-day list on its real occurredOn date instead of either leaking or disappearing.
  2. Double-counted projected balance in "All accounts." The projected future-balance deduction (added for the checking↔card link) was applied per-account inside the "All accounts" reduce, but summing it there double-counts the same debt: once as the checking account's projected payment, again as the card's own real (already-negative) balance, which never actually moves since no real payment transaction is recorded. Fixed by only applying the deduction when a single linked checking/ savings account is the active filter — the "All accounts" net total no longer applies it at all (a card's real balance already reflects the debt; there's nothing to project there).

Re-verified all scenarios via Playwright after both fixes: month-scoped "Credit card bills"/"Projected" sections, month navigation to the bill's due month, and the "All accounts" future-balance figure no longer doubling.

Third + fourth follow-up fixes (2026-07-08, same session): Overview's "Credit card debt" total, in two passes — the first pass introduced a regression the second pass had to correct.

Pass 1: totalCardDebt summed each card's real, all-time balance (accountBalances), which includes every charge ever made regardless of which bill it's tagged to — so a transaction dated this month but assigned (via the Bill dropdown) to next month's bill was still inflating this month's total. First fix: sum only the bill group (via groupTransactionsByBill) whose due date falls in the current calendar month.

Regression this introduced: the "Bill" dropdown always shows some selected value (defaulting to the natural next due date) and the create/ edit forms were submitting that value unconditionally — so even a normal transaction the user never touched the dropdown for got a non-null billDueDate persisted. Once the due day for a cycle has already passed (e.g. due day 3, transaction dated the 8th), the natural rollover lands next month — meaning ordinary, just-today expenses were silently excluded from "Credit card debt" too, which is what surfaced as "not considering an expense in today's date."

Root-cause fix (pass 2): Transactions.jsx's handleSubmit and handleEditSubmit now only send billDueDate when it actually differs from the freshly-computed natural default for that occurredOn — i.e. only when the user genuinely overrode it. Left at the default, billDueDate stays null, exactly like before the Bill dropdown existed. Overview.jsx's totalCardDebt was correspondingly simplified: walk each card's transactions from openingBalance, excluding only (a) future-dated ones (occurredOn > today, same as the existing real-balance convention) and (b) ones with an explicit billDueDate outside the current month — natural rollover alone no longer excludes anything. The per-card figure in "Credit cards" is unchanged (still shows the next bill regardless of month).

Verified via Playwright, three scenarios: (1) explicit override to next month → correctly excluded from the total; (2) ordinary today-dated charge on a card whose due day already passed this cycle (natural rollover, no override) → correctly included; (3) a same-month natural bill → still counts as before. All three consistent with no regressions.

Fifth follow-up fix (2026-07-08, same session): Transactions.jsx's "All accounts" balance totals and per-row visibility for credit cards. Two related gaps, both in the "All accounts" view:

  1. currentBalanceTotal/balanceAsOfDay/futureBalanceRaw summed every account including credit cards, so a card's debt was netted directly into "Current balance" — inconsistent with Overview's Total Balance, which had already been scoped to checking/savings only. Fixed by introducing the same liquidAccounts (CHECKING + SAVINGS) restriction for all three "All accounts" aggregates; filtering to one specific account (including a credit card directly) is unaffected.
  2. A credit card transaction whose bill fell outside the viewed range was falling through to the normal per-day list (from the earlier "bill leakage" fix) — but credit card transactions should never appear as individual rows in "All accounts," only via "Credit card bills." Split the old combined isAggregatedCreditCardTxn into isCreditCardTxn (a plain kind check, used to unconditionally exclude CC transactions from the per-day list) and kept the due-date-range check scoped to just creditCardBillGroups — so a transaction whose bill is out of range now simply doesn't appear anywhere in that view (not the day list, not the bill aggregate) rather than leaking into the day list.

Verified via Playwright with checking + linked credit card: "All accounts" Current balance now shows the checking-only total (matches Overview's Total Balance), the card's transaction appears only under "Credit card bills" (not as a day-list row), and filtering directly to the card still shows its own rows normally (unaffected).

Sixth follow-up fix (2026-07-08, same session): duplicate bill sections in Transactions.jsx. The fifth fix's creditCardBillGroups (real, old "Credit card bills" section, transaction-derived) and projectedBills (new "Projected" section, nextBillFor-derived) had converged to show the same thing for any linked card — both scoped to bills due within the viewed range — so a linked card's bill rendered twice. Merged into one section: creditCardBillGroups is gone; projectedBills now covers every credit card with a due day set when viewing "All accounts" (previously only cards with a linked payment account), and just the card(s) linked to the active account when filtering to one specific checking/savings account (unchanged from before). Added the "View all" button (previously only on the old section) to the merged "Projected" rows.

Verified via Playwright with one linked and one unlinked credit card: "All accounts" shows a single row per card (both linked and unlinked covered, each with "View all"), and filtering to the linked card's checking account narrows to just that card's bill, dropping the unrelated unlinked one.

Seventh follow-up fix (2026-07-08, same session, pre-existing bug): "View all" on a projected bill loaded the account's full history instead of just that bill. viewAccountTransactions only ever set an unbounded custom date range — fine for the original "View transactions" link (full history by design), but wrong for a specific bill row, since a bill is defined by billDueDateFor, not by an occurredOn range (an explicitly overridden transaction can be dated in a totally different month than its assigned bill, so no date-range filter could isolate it correctly anyway). Added a new filterBillDueDate state that filters filteredFlat by billDueDateFor(account, t) equality rather than by date range. viewAccountTransactions now takes an optional billDueDate param (passed from the "View all" button as p.dueDate) that sets this alongside the account filter. Cleared automatically whenever account, range, month, or custom-date-range controls are touched directly (search/category are left alone — narrowing within a bill still makes sense); a small chip ("Showing {date} bill only · View full history") shows near the balance stat when active, doubling as the way to clear it manually.

Verified via Playwright: two charges on the same card, same date, one tagged to July's bill and one to August's — "View all" on the (single, soonest) projected row now shows only the July-tagged charge, and "View full history" restores both.

Eighth follow-up fix (2026-07-08, same session): the "Amount owed" stat (and day-heading totals) still showed the card's full real balance while a specific bill was selected, instead of just that bill's total. Unlike the category/search filters — which deliberately leave "Current balance"/ "Amount owed" as the real, unfiltered figure (a longstanding, confirmed design decision) — selecting a specific bill via "View all" is a genuine scope change, so the balance stat needs to follow it. Added billBalanceAsOf(account, dueDateIso, cutoffDate), summing just that bill's transactions (via billDueDateFor) up to a cutoff date, and used it in place of accountBalanceAsOf for both currentBalanceTotal and balanceAsOfDay whenever filterBillDueDate is set. The "your real balance — not limited to..." disclaimer is now shown only for category/ search (not for the bill filter, since the balance genuinely is limited to the bill in that case — showing the disclaimer would be misleading).

Verified via Playwright: with a $20 July-bill charge and a $35 August-tagged charge on the same card (same day, $55 real balance), "View all" on the July bill now shows "Amount owed: $20.00" (matching the filtered list and day total) instead of the card's full $55.

Status as of end of session (2026-07-07, later)

Done, not yet committed: Credit cards feature (requested 2026-07-07, planned via plan mode, plan saved at /home/ricardo/.claude/plans/luminous-painting-pillow.md).

  • Backend: Account gained creditLimit (nullable BigDecimal) and dueDayOfMonth (nullable Integer, 1-31, a simple recurring day — no statement-cycle/billing-period model, decided explicitly). Both editable via CreateAccountRequest/UpdateAccountRequest, validated in AccountResource#validateCreditCardFields (400 on negative limit or out-of-range day). Both optional — a credit card account can exist without them.
  • Frontend: Accounts.jsx's add/edit forms conditionally show "Credit limit"/"Due day of month" fields when Kind = Credit Card (gated on kind at submit time, so switching kind away before saving discards stray values). New page frontend/src/pages/CreditCards.jsx (route /credit-cards, sidebar link after Accounts) — a glance dashboard: total owed/limit/available-credit summary panel, per-card utilization progress bar + status word ("Good standing"/"Near limit"/"Over limit", same 85%/100% thresholds as Budgets.jsx) reusing existing .track/ .budget-status CSS (no new styles needed), and a due-date line ("Due {date} · in N days") computed by rolling dueDayOfMonth forward to its next occurrence (clamped for short months). Gracefully omits the bar/ due-date line when those fields are null.
  • Also fixed: on Transactions.jsx, filtering to a single credit-card account now shows its balance as a positive "Amount owed" (label + sign + color all flip), consistently across the headline stat, each day-group total, and each row's running-balance figure — the underlying balance math (recomputeAccountBalance, openingBalance) is untouched, this is display-only. The "All accounts" net total (assets minus card debt) is intentionally left as a real negative-inclusive sum, unaffected.
  • Not done / explicitly deferred: no bulk/statement-history tracking, no "pay down" flow — this is glance/visibility only, same spirit as §8's deferred recurring-series tracking.

Status as of end of session (2026-07-07)

Done and committed: §1–§6 UI-wiring chunks (Categories page, Add-transaction form, Add/edit-budget form + Overview budget panel, Transactions filters + Export, Overview date-range dropdown) — 5 commits, 319c023 through c7af925. §7 Account edit/delete is also done and committed (d5aacaa).

Also done and committed: §7 Category edit/delete (backend PUT/DELETE /categories/{id} + GET /categories/{id}/usage, frontend edit/delete rows in Categories.jsx, plus a follow-up: category color picker now auto-picks a random non-repeating color from a 10-color palette on "Add category" instead of showing 5 fixed presets, with a "Choose color" toggle to override manually) and §7 Budget delete (backend DELETE /budgets/{id} — no usage-check needed, nothing references a Budget row; frontend edit/delete icons on budget-card, pencil reopens the existing "Set budget" form pre-filled).

Done, not yet committed: §7 User profile page — see §7 item 5 below for full detail — plus two bug fixes found along the way:

  1. client.js's global 401 interceptor was catching failed-login 401s (bad credentials) as if they were expired-session 401s, forcing a hard window.location.href = '/login' reload instead of letting Login.jsx show its inline error. Fixed by excluding /auth/* requests from that handler.
  2. The new PUT /users/me/password's "wrong current password" case originally returned 401, which would have tripped that same interceptor for an already-logged-in user. Changed to 400 (the user IS authenticated; this is a request-validation failure, not a session issue).

Also added an account_history table + "Account history" card on the Profile page: every display-name/email change is logged old→new, password changes log only the date (never a value). See §7 item 5.

Also done and committed: §7 Transaction edit/delete (the last item in §7 — all of §7 is now complete) plus three follow-up fixes/features on the Transactions page found via user testing. See §7 item 1 for full detail on all of these:

  1. Added Account.openingBalance (immutable, set once at creation) so account.balance and every transaction's runningBalance are fully re-derived in true chronological (occurredOn, then id) order on every create/edit/delete, via a new TransactionResource#recomputeAccountBalance helper — replacing the old insertion-order-based math, which was subtly wrong for backdated transactions.
  2. Fixed a client-side timezone bug: new Date("yyyy-MM-dd") parses as UTC midnight, which renders as the previous calendar day in timezones behind UTC — a transaction dated "today" was showing under "Yesterday." Fixed with a shared frontend/src/utils/date.js#parseLocalDate used everywhere occurredOn strings are turned into Date objects (Transactions.jsx and Overview.jsx both had the bug).
  3. Added a "Current balance" stat to the Transactions page (real account balance, respects the account filter, unaffected by category/search — by design, confirmed with user) and changed each day-group's figure from "net sum for that day" to "balance as of that day." When a category filter is active, a second "<category> total" stat now also appears (net sum of the currently filtered rows) — this one does reflect search/account/range filters too, since it's explicitly a filtered subtotal rather than a real balance.

Reference pattern for delete UIs (see [[project-accounts-pattern]] in memory): in-card/in-row swap to a "Delete X? This cannot be undone." confirm panel (no window.confirm, no app-wide modal) using the .btn-red style, and a pre-validation check before showing the confirm step at all (e.g. GET /categories/{id}/usage) rather than attempting the delete and showing an error after — skip the pre-check only when nothing could possibly reference the row (Budget's and Transaction's case).

Not started: §7 is fully complete, and so is the recurrence/installments feature below — nothing outstanding right now beyond §5's demo-login bug, §6's backend gaps, and the new §8 item below (neither urgent).

8. Recurring-series tracking at the DB level — [x] done (raised 2026-07-07, resolved 2026-07-11)

Was: a repeat/installment purchase generated N independent Transaction rows linked only by a cosmetic seriesInfo string (e.g. "3/12"), no real series_id/group column anywhere — editing/deleting one occurrence only ever touched that single row, deleting one left a gap in the numbering, no "edit/delete this and all future occurrences" option.

Now fixed — see the 2026-07-11 session entry above for full detail:

  • Real series_id (Java-generated UUID, V8__transaction_series_id.sql) set at generation time in TransactionResource#create.
  • seriesInfo renumbers contiguously after any delete; a series collapsed to one row clears its series fields entirely.
  • "This occurrence only" / "This and future" scope choice on both edit and delete, frontend + backend, including INSTALLMENTS-aware amount re-splitting.

Done, not yet committed: Recurrence/installments (requested 2026-07-07).

  • Backend: Transaction gained seriesInfo (nullable String, e.g. "3/12", display-only — no series/group table). POST /transactions now returns List<Transaction> instead of a single Transaction (frontend never consumed the return value beyond cache invalidation, so this was safe). CreateTransactionRequest gained repeat (RepeatFrequency enum: NONE/ WEEKLY/MONTHLY/YEARLY/INSTALLMENTS) and occurrences (2-60, validated). Confirmed approach: pre-generate all occurrences upfront as real rows (no background @Scheduled job, no new dependency — quarkus-scheduler isn't in this project and adding it was explicitly deferred). WEEKLY/ MONTHLY/YEARLY repeat the same amount each occurrence; INSTALLMENTS splits the total evenly (remainder cent(s) land on the last installment so the parts always sum back exactly). Each generated row is a normal transaction, editable/deletable with the §7 item 1 machinery already built — no bulk/series management exists, by design (kept simple).
  • Frontend: "Repeat" select + conditional "Occurrences" input on the add-transaction form, with inline help text explaining what will be created. Each txn-row shows a seriesInfo badge (reusing the existing .tag class) when present, e.g. "3/12".

Environment reminders for next session (see project-dev-environment in memory for more):

  • Local dev runs via docker compose — containers ledger-backend (8080), ledger-frontend (5173), ledger-db (5432) — check docker ps before assuming anything needs starting.
  • The real logged-in user is ricardompcampos@hotmail.com, not the seeded demo@ledger.app account.
  • Demo login is still broken (seed-data.sql's bcrypt hash uses an incompatible $2b$ prefix) — not fixed, see §5.
  • Editing any file under backend/src/main/java while quarkus:dev is running wipes the local database (Hibernate drop-and-create on live reload) — expect this, not a bug. User has said to just proceed without pausing for confirmation each time, since local data is disposable.

Snapshot from a full frontend + backend audit (2026-07-06). The backend is further along than it looks from the UI — Transactions, Budgets, and Categories all have working create endpoints that are simply never called. The gap is almost entirely in the frontend; a couple of backend gaps are called out separately at the end.

Reference pattern for "how a working button looks in this codebase": frontend/src/pages/Accounts.jsxuseState toggle for a panel + controlled form inputs + useMutation (react-query) + queryClient .invalidateQueries on success. There is no shared <Modal> or <Dropdown> component yet, so the first one built should probably be extracted for reuse rather than copy-pasted three more times.

1. Categories — no UI at all (highest priority, blocks the rest)

Backend is fully ready: Category entity, GET /categories, POST /categories (backend/src/main/java/com/ledger/category/CategoryResource.java). Frontend CategoriesApi.list() / .create() already exist in frontend/src/api/ledger.js:14-17 but are never imported anywhere.

  • Add a Categories page (frontend/src/pages/Categories.jsx) with list + create form (name, color), using the Accounts.jsx pattern.
  • Add /categories route in App.jsx.
  • Add "Categories" link to Sidebar.jsx (links array, currently only Overview/Transactions/Budgets/Accounts).
  • Backend: add PUT /categories/{id} and DELETE /categories/{id} — currently create/list only, no edit or delete.
  • Once this exists, wire the Transactions category filter/picker to CategoriesApi.list() instead of the hardcoded categoryColors object (Transactions.jsx:112-117).

2. Transactions page — "Add transaction" is a dead button

Transactions.jsx:86-89 — no onClick, no form/modal exists anywhere. Backend POST /transactions already works (backend/src/main/java/com/ledger/transaction/TransactionResource.java:36), required fields: accountId, description, amount; optional: categoryId, occurredOn (defaults to today). Note it also updates the account's running balance server-side as a side effect. Frontend TransactionsApi.create already exists in ledger.js:22.

  • Build the add-transaction form/panel (account select, category select populated from CategoriesApi.list(), description, amount, date).
  • Wire it to TransactionsApi.create + invalidate the relevant queries (account list for balance, transactions list).
  • Wire the account filter select (Transactions.jsx:104-109) — currently renders options but has no onChange/state.
  • Wire the category filter select (Transactions.jsx:112-117) — same issue, also depends on item 1. Now populated from CategoriesApi.list().
  • Wire the date-range select ("Last 30 days" / "Last 90 days" / "This year", Transactions.jsx:120-124) — filtering is still done client-side over all fetched transactions (no backend date-range params yet, see §6), but the control now actually filters the visible list.
  • Wire or remove the "Export" button (Transactions.jsx:127-130) — exports the currently filtered transactions as a CSV download.

3. Budgets page — no way to create or edit a budget

Budgets.jsx:51-54 — "Edit budgets" button has no onClick, and there's no "Add budget" affordance either. Backend POST /budgets is a working upsert-by-category-and-month (backend/src/main/java/com/ledger/budget/BudgetResource.java:67), needs categoryId, month, limitAmount. Frontend BudgetsApi.upsert already exists in ledger.js:30.

  • Build a set/edit-limit form (category select, month, limit amount), reusing the Accounts.jsx pattern, calling BudgetsApi.upsert.
  • Budgets.jsx:24 hardcodes yearMonth to the current month (new Date().toISOString().slice(0, 7)) — no way to view or set a budget for any other month. Add month navigation.
  • Overview.jsx:118-124 has a placeholder panel ("Category breakdowns will live here once budgets have real spend data behind them") even though GET /budgets/month/{yearMonth}/spend already returns exactly that data — just needs to be fetched and rendered (as cards/progress bars per CLAUDE.md, not charts).

4. Overview page — "This month" button does nothing

Overview.jsx:51-54 — static button, no onClick, no dropdown markup, no date-range state anywhere on the page.

  • Build the dropdown (This week / Last week / This month / Last 30 days / This year) — added as a reusable frontend/src/components/Dropdown.jsx (generic trigger + menu, click-outside-to-close), styled to match .btn-ghost and the hairline-border/no-shadow language.
  • Wire selection to the "Recent activity" list — filtered client-side over the already-fetched per-account transactions (same approach as the Transactions date-range filter in §2/§4; no backend date-range endpoint needed at current data volumes, see §6).

5. Security / data fixes found and applied during implementation

  • User.passwordHash had no @JsonIgnore — every endpoint that embeds a User relation (Category, Account, Transaction, Budget) was leaking the bcrypt hash in plain JSON. Fixed in backend/src/main/java/com/ledger/user/User.java.
  • seed-data.sql's transaction INSERTs referenced the t (VALUES) alias inside the categories JOIN condition before t was introduced in the FROM clause — fails on Postgres ("missing FROM-clause entry for table t"). Fixed by reordering the JOINs so t is introduced before it's referenced.
  • seed-data.sql's demo password hash uses a $2b$ bcrypt prefix that Quarkus's Elytron BcryptUtil doesn't recognize (ELY08003: Unknown crypt string algorithm) — demo login (demo@ledger.app/demo12345) is currently broken. Needs a hash regenerated via BcryptUtil.bcryptHash (Elytron-compatible, likely $2a$ prefix) rather than a hash lifted from another bcrypt implementation.

Note: editing any file under backend/src/main/java while quarkus:dev is running triggers a live-reload schema regeneration (drop-and-create, per the Known Gaps section above) — it silently wipes all local data. Not a bug to fix, just something to expect until real migrations exist.

6. Backend gaps

  • No date-range query params on GET /transactions/account/{accountId} — returns everything, unbounded, no pagination. Needed for both the Overview dropdown and the Transactions date-range select to be meaningful server-side rather than client-side filtering over the full history.
  • No dashboard/stats aggregation endpoint — Overview currently computes totals client-side from full per-account transaction lists fetched one account at a time. Fine for now at small data volumes, but worth an endpoint if this needs to scale.
  • No DELETE/PUT for Transaction or Budget (Budget's create doubles as update via upsert, so this is lower priority than Category's missing edit/delete).

Suggested order (all done as of 2026-07-06)

  1. Categories page (unblocks category pickers everywhere else).
  2. Add-transaction form.
  3. Add/edit-budget form + wire the Overview budget-spend panel (data already exists server-side, purely a frontend job).
  4. Transactions filters (account/category/date) + Export.
  5. Overview "This month" dropdown (client-side range filter).

All five UI-wiring chunks from this audit are implemented. What's left is §6's backend gaps (date-range/pagination endpoint, Category edit/delete, Transaction/Budget delete) and the outstanding bcrypt-prefix bug in §5 (demo login) — none of these block normal use of the app with a real account, they're follow-ups for later.

7. Edit/delete + profile management (requested 2026-07-06, not yet built)

Every entity currently only supports create + list. There is no edit or delete UI anywhere, and the only delete endpoint that exists server-side (DELETE /accounts/{id}) isn't even called from the frontend. None of this is built yet — tracked here for a future round of chunks.

  1. Transactions — [x] done, committed.

    • Backend: Account gained an immutable openingBalance field (set once at creation). PUT/DELETE /transactions/{id} added, both calling a new recomputeAccountBalance() helper that re-derives account.balance and every transaction's runningBalance from openingBalance, walking transactions in chronological (occurredOn, then id) order — not insertion order like the old create() did. seed-data.sql updated with opening_balance values for the demo accounts.
    • Frontend: pencil/trash icons on each txn-row, same in-row edit-form / confirm-panel pattern as the rest of the app. No pre-check before delete (nothing references a transaction, same as Budget's case).
    • Follow-ups from user testing: fixed a UTC/local timezone bug in day grouping (frontend/src/utils/date.js#parseLocalDate), added a "Current balance" stat + changed day-group totals to "balance as of that day" instead of a same-day net sum, and added a category-filtered total stat. See the note above for full detail.
  2. Budgets — [x] done, awaiting user review/commit.

    • Backend: added DELETE /budgets/{id}.
    • Frontend: edit/delete icons on budget-card — pencil reopens the top "Set budget" form pre-filled with that category+limit (still upserts via the same endpoint), trash swaps to the in-card confirm panel.
  3. Accounts — [x] done, committed (d5aacaa).

    • Backend: added PUT /accounts/{id} (name/institution/kind only — balance stays server-derived from transactions). DELETE now catches the FK-constraint case and returns 409 instead of a raw 500.
    • Frontend: edit/delete icon buttons on account-card-lg, in-card confirm panel, pre-validates via TransactionsApi.listByAccount before showing the confirm step (see delete-pattern note above).
  4. Categories — [x] implemented, awaiting user review/commit.

    • Backend: added PUT /categories/{id}, GET /categories/{id}/usage (transaction + budget counts), DELETE /categories/{id} — blocks (409) if used by any transaction or budget rather than nulling out category_id on existing rows (budgets require a category — Budget.category is optional = false — so nulling wasn't an option there anyway, and blocking keeps both entities' delete behavior consistent).
    • Frontend: edit/delete rows in Categories.jsx, same in-row swap pattern as Accounts, pre-validates via the new /usage endpoint.
  5. User profile (email, password, display name) — [x] done, awaiting user review/commit.

    • Backend: new UserResourcePUT /users/me (display name + email, uniqueness-checked like signup, re-issues the JWT since the token's upn is the email), PUT /users/me/password (requires current password via BcryptUtil.matches, 400 not 401 on mismatch). Extracted the duplicated JWT-issuing code out of AuthResource into security/TokenService since a second call site needed it.
    • Frontend: new /profile page + sidebar link, two panels (profile info, change password), same form pattern as everywhere else. AuthContext got applySession() to refresh the cached token/user after a profile save.
    • Bonus: account_history table + "Account history" card logging display-name/email changes (old→new) and password changes (date only) — see note above.

Suggested build order: Account edit/delete, Category edit/delete, Budget delete, and User profile — [x] all four done, see above — leaving only Transaction edit/delete, the trickiest one (running-balance recomputation on delete/edit affects every later transaction on that account).