61 KiB
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 fromprivateto package-private soTransactionResourcecan reuse it. NewTransactionResource#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 bothcreate()'s batch-generation loop (replacing the oldrepeat == NONE ? request.billDueDate : nullline) andapplyToSeries()(replacing a flatt.billDueDate = request.billDueDatethat 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): theBilldropdown (relabeled "First bill" when a repeat is selected) now always renders regardless ofrepeat, laid out on its own row whenOccurrencesis also showing;isOverrideinhandleSubmitno longer gates onrepeat === '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
Transaction—seriesId(JavaUUID.randomUUID().toString(),VARCHAR(36),V8__transaction_series_id.sql, indexed) andseriesRepeat(theRepeatFrequencythe batch was created with,V9__transaction_series_repeat.sql) — both set once per batch inTransactionResource#create, identically across every generated row, both null for one-off transactions (no backfill for pre-existing rows — they keep theirseriesInfodisplay string and fall back to plain single-row edit/delete, per §8's already-accepted tradeoff). - New
EditScopeenum (THIS/FUTURE).PUT /transactions/{id}gained an optionalscopebody field (defaultTHIS, backward compatible);DELETE /transactions/{id}gained an optional?scope=query param (defaultTHIS).FUTUREis only honored when the target row has a non-nullseriesId.- Edit "this and future":
description/category/billDueDateflat-set across every row from the edited occurrence onward (via newseriesFrom(anchor),occurredOn-then-idordered).occurredOnis never propagated — only the anchor row's own date changes.amounton anINSTALLMENTSseries re-splits the entered total evenly across the remaining rows (reuses the existingsplitAmounthelper, 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'seriesInfocontiguously (closing gaps like1/12,2/12,4/12,5/12→1/11,2/11,3/11...), and clearsseriesId/seriesInfo/seriesRepeatentirely if a series collapses to one remaining row (it silently reverts to a plain non-series transaction — no scope UI shown for it anymore).
- Edit "this and future":
- Frontend (
Transactions.jsxand its line-for-line duplicate block inCardBills.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 anINSTALLMENTSrow, a one-line re-split hint — only whent.seriesIdis 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.removegained a defaultedscopeparam ('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:
- Overview: "Checking & Savings accounts" list was
kind !== 'CREDIT_CARD'(leaked INVESTMENT accounts in) instead of reusingliquidAccounts. 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. - Backend: added
Transaction.createdAt(V5migration,Instant,@PrePersist-guaranteed on every insert path incl. repeat/installment batches andCreditCardBillSyncService's system rows) — needed for #3.columnDefinitioncarries theDEFAULT now()into Hibernate's dev-mode DDL too, not just the Flyway migration, soseed-data.sql(which doesn't set the column) keeps working in dev. - 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 bycreatedAtdesc (id as tiebreaker), ignores range entirely, excludeslinkedCardbill-sync rows. This made the date-range dropdown itself dead (it had no other consumer) — removed from Overview's header along withrangeOptions/rangeBounds/startOfWeek. - 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. - CreditCards.jsx "Available credit" (
totalLimit - totalOwed) mixed mismatched sets:totalLimitonly summed cards with a limit, buttotalOwedsummed all cards — a no-limit card's debt silently ate into another card's available credit. Fixed by summing owed only overcardsWithLimitfor 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 (vianextBillFor), matching the pattern Transactions.jsx's "View bill" link already used, instead of relying onCardBills.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:
- 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 (reusinggroupTransactionsByBill/nextBillForfromutils/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. - Stripped
Transactions.jsxfrom 1244 → 888 lines: credit card accounts filtered out ofaccountsright after the query (cascades to the account dropdown,txnQueries, every downstream calc), then deletedbillOptionsFor/billOptionsWithCurrent,filterBillDueDate,billBalanceAsOf,isCreditCardFiltersign-flip,projectedCcDeduction,aggregateCreditCards/isCreditCardTxn,projectedBills,cardBillDates/defaultBillDueDate/stepBill,viewAccountTransactions,resetToDefaultView, and both Bill dropdowns. Small added robustness: a staleledger:lastAccountIdpointing 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.linkedCardFK (V4__transaction_linked_card.sql) marks a row as system-generated. NewCreditCardBillSyncService(com.ledger.anthropic,@ApplicationScoped) ownsrecomputeAccountBalance(moved out ofTransactionResource, same algorithm, now has two callers) andsync(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 ofutils/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 fromTransactionResource's create/update/ delete and unconditionally at the end ofAccountResource#update.TransactionResourcerejects directPUT/DELETEon a transaction withlinkedCard != null(400). - Frontend:
Transactions.jsxrenders alinkedCardrow 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 sameGET /transactions/account/{accountId}the page already calls.CardBills.jsxneeded no changes (synced rows live on the payment account, never on the card itself). - One bug found and fixed during verification:
CreditCardBillSyncService #syncsetrow.amountafterrow.persist()for a newly-created row — IDENTITY-strategy entities insert immediately onpersist()(can't defer for a generated id), soamount(NOT NULL) has to be set first. Fixed by assigning every field beforepersist().
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-derivedcardBillDatesmemo — 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')movesfilterBillDueDateto the adjacent entry incardBillDates, 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 From–To 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
showMoreFiltersstate), 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:
filterBillDueDatestill 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:
Transaction.billDueDate(backend) — new nullableLocalDatecolumn (V2__transaction_bill_due_date.sql), settable/clearable viaCreateTransactionRequest/UpdateTransactionRequest. Display/grouping tag only, same spirit asseriesInfo— never read byrecomputeAccountBalance. For repeat/installment transactions the override is only applied to the batch's semantics correctly: every generated row getsbillDueDate = nullregardless of what was sent (a fixed date copied onto every installment would be wrong), verified via curl.- 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 inTransactions.jsx. Add/edit transaction forms show a "Bill" dropdown (3 upcoming due dates) when the account is a credit card withdueDayOfMonthset — 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. - 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.
Account.paymentAccount+ projected bill row — CREDIT_CARD accounts can link a CHECKING/SAVINGS "payment account" (V3__account_payment_account.sql, validated inAccountResource#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 realTransaction(no id, no edit/delete), and only affects the projected/ future balance stat (via a newprojectedCcDeductionapplied solely tofutureBalanceRaw), never the realcurrentBalanceTotal/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:
- 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 havingisAggregatedCreditCardTxnalso 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 realoccurredOndate instead of either leaking or disappearing. - 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:
currentBalanceTotal/balanceAsOfDay/futureBalanceRawsummed 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 sameliquidAccounts(CHECKING + SAVINGS) restriction for all three "All accounts" aggregates; filtering to one specific account (including a credit card directly) is unaffected.- 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
isAggregatedCreditCardTxnintoisCreditCardTxn(a plain kind check, used to unconditionally exclude CC transactions from the per-day list) and kept the due-date-range check scoped to justcreditCardBillGroups— 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:
AccountgainedcreditLimit(nullable BigDecimal) anddueDayOfMonth(nullable Integer, 1-31, a simple recurring day — no statement-cycle/billing-period model, decided explicitly). Both editable viaCreateAccountRequest/UpdateAccountRequest, validated inAccountResource#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 pagefrontend/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-statusCSS (no new styles needed), and a due-date line ("Due {date} · in N days") computed by rollingdueDayOfMonthforward 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:
client.js's global 401 interceptor was catching failed-login 401s (bad credentials) as if they were expired-session 401s, forcing a hardwindow.location.href = '/login'reload instead of lettingLogin.jsxshow its inline error. Fixed by excluding/auth/*requests from that handler.- 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:
- Added
Account.openingBalance(immutable, set once at creation) soaccount.balanceand every transaction'srunningBalanceare fully re-derived in true chronological (occurredOn, thenid) order on every create/edit/delete, via a newTransactionResource#recomputeAccountBalancehelper — replacing the old insertion-order-based math, which was subtly wrong for backdated transactions. - 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 sharedfrontend/src/utils/date.js#parseLocalDateused everywhereoccurredOnstrings are turned intoDateobjects (Transactions.jsxandOverview.jsxboth had the bug). - 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 inTransactionResource#create. seriesInforenumbers 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:
TransactiongainedseriesInfo(nullable String, e.g."3/12", display-only — no series/group table).POST /transactionsnow returnsList<Transaction>instead of a singleTransaction(frontend never consumed the return value beyond cache invalidation, so this was safe).CreateTransactionRequestgainedrepeat(RepeatFrequencyenum: NONE/ WEEKLY/MONTHLY/YEARLY/INSTALLMENTS) andoccurrences(2-60, validated). Confirmed approach: pre-generate all occurrences upfront as real rows (no background@Scheduledjob, no new dependency —quarkus-schedulerisn'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-rowshows aseriesInfobadge (reusing the existing.tagclass) 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— containersledger-backend(8080),ledger-frontend(5173),ledger-db(5432) — checkdocker psbefore assuming anything needs starting. - The real logged-in user is
ricardompcampos@hotmail.com, not the seededdemo@ledger.appaccount. - 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/javawhilequarkus:devis running wipes the local database (Hibernatedrop-and-createon 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.jsx — useState 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
/categoriesroute inApp.jsx. - Add "Categories" link to
Sidebar.jsx(linksarray, currently only Overview/Transactions/Budgets/Accounts). - Backend: add
PUT /categories/{id}andDELETE /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 hardcodedcategoryColorsobject (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 noonChange/state. - Wire the category filter select (
Transactions.jsx:112-117) — same issue, also depends on item 1. Now populated fromCategoriesApi.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:24hardcodesyearMonthto 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-124has a placeholder panel ("Category breakdowns will live here once budgets have real spend data behind them") even thoughGET /budgets/month/{yearMonth}/spendalready 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-ghostand 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.passwordHashhad no@JsonIgnore— every endpoint that embeds aUserrelation (Category, Account, Transaction, Budget) was leaking the bcrypt hash in plain JSON. Fixed inbackend/src/main/java/com/ledger/user/User.java.seed-data.sql's transaction INSERTs referenced thet(VALUES) alias inside thecategoriesJOIN condition beforetwas introduced in the FROM clause — fails on Postgres ("missing FROM-clause entry for table t"). Fixed by reordering the JOINs sotis introduced before it's referenced.seed-data.sql's demo password hash uses a$2b$bcrypt prefix that Quarkus's ElytronBcryptUtildoesn't recognize (ELY08003: Unknown crypt string algorithm) — demo login (demo@ledger.app/demo12345) is currently broken. Needs a hash regenerated viaBcryptUtil.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/PUTfor 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)
- Categories page (unblocks category pickers everywhere else).
- Add-transaction form.
- Add/edit-budget form + wire the Overview budget-spend panel (data already exists server-side, purely a frontend job).
- Transactions filters (account/category/date) + Export.
- 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.
-
Transactions — [x] done, committed.
- Backend:
Accountgained an immutableopeningBalancefield (set once at creation).PUT/DELETE /transactions/{id}added, both calling a newrecomputeAccountBalance()helper that re-derivesaccount.balanceand every transaction'srunningBalancefromopeningBalance, walking transactions in chronological (occurredOn, thenid) order — not insertion order like the oldcreate()did.seed-data.sqlupdated withopening_balancevalues 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.
- Backend:
-
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.
- Backend: added
-
Accounts — [x] done, committed (
d5aacaa).- Backend: added
PUT /accounts/{id}(name/institution/kind only — balance stays server-derived from transactions).DELETEnow 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 viaTransactionsApi.listByAccountbefore showing the confirm step (see delete-pattern note above).
- Backend: added
-
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 outcategory_idon existing rows (budgets require a category —Budget.categoryisoptional = 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/usageendpoint.
- Backend: added
-
User profile (email, password, display name) — [x] done, awaiting user review/commit.
- Backend: new
UserResource—PUT /users/me(display name + email, uniqueness-checked like signup, re-issues the JWT since the token'supnis the email),PUT /users/me/password(requires current password viaBcryptUtil.matches, 400 not 401 on mismatch). Extracted the duplicated JWT-issuing code out ofAuthResourceintosecurity/TokenServicesince a second call site needed it. - Frontend: new
/profilepage + sidebar link, two panels (profile info, change password), same form pattern as everywhere else.AuthContextgotapplySession()to refresh the cached token/user after a profile save. - Bonus:
account_historytable + "Account history" card logging display-name/email changes (old→new) and password changes (date only) — see note above.
- Backend: new
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).