6.5 KiB
6.5 KiB
Ledger — Project Context
Personal finance app. Read this before making changes — it captures decisions made during design and architecture discussion that aren't otherwise obvious from the code.
Design language — do not deviate without asking
- Dark mode only. No light theme, no toggle.
- The "ledger" concept is the whole point. Every monetary figure is set in monospace (IBM Plex Mono), right-aligned, tabular. This is the one non-negotiable rule — if you add a new figure anywhere (a new stat, a new card), it follows this rule too.
- Fraunces (serif) is reserved for the hero balance number and page titles only. Do not use it for body text or labels.
- No charts. Budgets and spending are shown as cards with progress bars and status words (On track / Near limit / Over budget). This was an explicit choice.
- Palette is muted, not neon: jade
#4FA98Afor positive, brick-red#C75450for negative, gold#C9A227reserved for "near limit" warnings only. Background is ink-navy#0E1116, not true black. - Hairline borders (
#262C36) do the separating — avoid drop shadows or heavy card elevation. - Tokens live in
frontend/src/styles/tokens.scss. If the UI and tokens disagree, fix the UI, not the tokens.
Architecture decisions
- Backend: Quarkus (Java 21, Maven). Explicit choice for native-image builds via GraalVM. Don't suggest swapping to Spring Boot or Express.
- IDENTITY, not Hibernate SEQUENCE, for entity IDs. So plain SQL (seed scripts, manual fixes) never has to guess Hibernate's sequence names. Keep this pattern for any new entity.
- Monorepo:
/frontendand/backendin one repo, deployed as two separate artifacts. - Auth: JWT via SmallRye JWT, 7-day bearer tokens, no refresh flow. BCrypt via
quarkus-elytron-security-common'sBcryptUtil(not jBCrypt, always$2a$prefix). - Budget spend is computed live from transactions (
GET /budgets/month/{yyyy-MM}/spend), not stored — don't add a cached "spent" column to Budget, it'll drift. - Balance math:
Account.openingBalance(immutable, set at creation) +recomputeAccountBalance()inCreditCardBillSyncService(moved out ofTransactionResourceonce a second call site needed it) — re-derivesaccount.balanceand everyrunningBalancein chronological order (occurredOn, thenid) on every create/edit/delete. Never touch insertion-order-based math. - Flyway: active in prod (
%prod.quarkus.flyway.migrate-at-start=true). Dev usesdrop-and-create. Migrations live inbackend/src/main/resources/db/migration/. - Repeat/installments: pre-generated as flat
Transactionrows at create time (no scheduler, no series table).seriesInfo(e.g."3/12") is display-only. No bulk edit/delete by design. - Credit cards:
Accounthas optionalcreditLimit(BigDecimal) anddueDayOfMonth(Integer 1-31). No statement-cycle model — just a recurring day. Validated inAccountResource#validateCreditCardFields. - Credit card bills project onto their linked payment account as real transactions.
CreditCardBillSyncService#sync(card)maintains oneTransactionper open bill (dated on its due date, holding the bill's total) on the card'spaymentAccount, marked viaTransaction.linkedCard. Called after every create/edit/delete of a card's own transactions and after anyAccountResourceupdate (safe to call unconditionally — self-corrects a changed/cleared payment account, due day, or kind). These rows flow through normal balance math for free;TransactionResourcerejects directPUT/DELETEon one (linkedCard != null→ 400), and the frontend renders them dashed with no edit/clone/delete, just a "View bill" link to Card Bills. Chosen over an earlier client-side-only "Projected" row after that approach caused repeated double-counting/month-leakage bugs. - TokenService: extracted from
AuthResourceintosecurity/TokenService— bothAuthResourceandUserResource(after profile email change) use it to issue JWTs. - Delete pattern: in-card/in-row confirm panel (no
window.confirm, no modal), pre-validates before showing confirm step when something might reference the row (e.g.GET /categories/{id}/usage). Skip pre-check only when nothing could reference the row (Budget, Transaction).
Dev environment
- Local dev runs via Docker Compose — containers
ledger-backend(8080),ledger-frontend(5173),ledger-db(5432). Checkdocker psbefore assuming anything needs starting. - Editing any
.javafile whilequarkus:devis running wipes the local database (Hibernatedrop-and-createon live-reload). Expected. Local data is disposable. - The real logged-in user is
ricardompcampos@hotmail.com, not the seededdemo@ledger.appaccount. - CORS in dev allows
http://localhost:5173and the ngrok dev URL (flattop-depth-dropper.ngrok-free.dev). - Prod CORS is locked to
https://ledger-finance.darkroasted.vps-kinghost.net.
Deployment
- CI: GitHub Actions on self-hosted runners (
graalvm-25for backend,easynode-debianfor frontend/deploy) - Secrets: Doppler (
prdconfig) —DOPPLER_AT_SECRETSGitHub secret is the only secret in GH Actions - Docker Hub:
rmcampos/ledger-backend(versionedvYYYY.MM.DD.<run_number>+latest),rmcampos/ledger-frontend(latestonly) - Deploy workflow: triggered after Backend CI or Frontend CI completes → Terraform plan+apply to Kubernetes
- Terraform state: Cloudflare R2, bucket
ledger-finance - DB backups: Kubernetes CronJob → R2 bucket
ledger-finance-backups, twice daily
Known gaps / deferred
- Demo login broken:
seed-data.sqlhash uses$2b$prefix, Elytron wants$2a$. Fix: regenerate hash viaBcryptUtil.bcryptHash. - No refresh tokens — re-login after 7 days.
- No date-range query params on
GET /transactions/account/{id}— filtering is client-side over full history. - No
series_idon Transaction — repeat/installment occurrences share onlyseriesInfostring. Consequence: deleting one leaves a gap in numbering; no "delete all future" affordance. Tracked in TODO.md §8. - No dashboard aggregation endpoint — Overview computes totals client-side.
Commands
# Preferred: full stack via Docker Compose
docker compose up
# Backend only (local JVM)
cd backend && ./mvnw quarkus:dev # dev server, localhost:8080
./mvnw package -Pnative # native build
# Frontend only (local Node)
cd frontend && npm install && npm run dev # localhost:5173
# Seed data (after backend has created the schema)
psql -h localhost -U ledger -d ledger -f backend/seed-data.sql