57 Commits

Author SHA1 Message Date
TehRiehlDeal 062b807732 Fix ImportStatementDialog overflow on narrow viewports
CI / secrets-scan (push) Successful in 7s
CI / sast (push) Successful in 15s
CI / vuln-scan (push) Successful in 16s
CI / test (push) Successful in 28s
CI / lint (push) Successful in 34s
CI / build-images (push) Successful in 2m7s
CI / image-scan (push) Successful in 55s
CI / push (push) Has been skipped
CI / secrets-scan (pull_request) Successful in 7s
CI / sast (pull_request) Successful in 15s
CI / vuln-scan (pull_request) Successful in 16s
CI / test (pull_request) Successful in 26s
CI / lint (pull_request) Successful in 33s
CI / build-images (pull_request) Successful in 2m4s
CI / image-scan (pull_request) Successful in 53s
CI / push (pull_request) Has been skipped
The review and confirm steps were rendering as wide editable tables
inside a `sm:max-w-3xl` dialog (~768px). The inputs alone consumed
~750px before padding, so on smaller desktop windows the content was
clipped left/right, and on mobile it was effectively unusable.

Two-pronged fix:

- Widen the dialog itself to `md:max-w-5xl xl:max-w-6xl` so the table
  has room on typical desktops. Mobile stays full-width minus margin.
- Split each step into responsive layouts gated by Tailwind's `md:`
  breakpoint. Desktop keeps the editable table (now with fixed-width
  columns, an inline status badge instead of a separate Status column,
  and `overflow-auto` as a graceful fallback at intermediate widths).
  Below `md:`, rows render as stacked cards — checkbox + date + status
  on top, description full-width, amount + type wrapped, transfer
  destination dropping below when applicable. Same data, no horizontal
  scrolling on phones.

The Confirm step's expand-to-preview table gets the same treatment:
table on desktop, summary cards on mobile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:44:33 -07:00
TehRiehlDeal 9f0af6bfb8 Fix backend lint errors in the statement-parser code
CI / secrets-scan (push) Successful in 6s
CI / sast (push) Successful in 16s
CI / vuln-scan (push) Successful in 21s
CI / test (push) Successful in 27s
CI / lint (push) Successful in 33s
CI / build-images (push) Successful in 2m10s
CI / image-scan (push) Successful in 53s
CI / push (push) Has been skipped
CI / secrets-scan (pull_request) Successful in 6s
CI / sast (pull_request) Successful in 17s
CI / vuln-scan (pull_request) Successful in 19s
CI / test (pull_request) Successful in 26s
CI / lint (pull_request) Successful in 33s
CI / build-images (pull_request) Successful in 2m13s
CI / image-scan (pull_request) Successful in 53s
CI / push (pull_request) Has been skipped
The new statements module was hitting type-safety errors from
typescript-eslint's recommendedTypeChecked config:

- parse-statement.dto.ts: tighten the Transform decorator's signature so
  the JSON.parse path returns a typed object or undefined, not `any`.
- duplicate-detector.service.ts: drop the unused
  WEAK_DESCRIPTION_SIMILARITY constant left over from earlier logic.
- csv.parser.ts and ofx.parser.ts: the parse() methods were `async`
  without any `await` (require-await). Convert them to non-async
  functions that return a Promise — wrap parseSync() in a try/catch so
  thrown errors still surface as rejected promises for spec callers
  that use `.rejects.toThrow()`.
- ofx.parser.ts: replace `require('node-ofx-parser')` with a typed
  `import * as ofxLib`, backed by a hand-written declaration file at
  src/types/node-ofx-parser.d.ts that captures the bank + credit-card
  transaction shapes we consume.
- pdf.parser.ts: import the typed `PDFParse` class from pdf-parse
  directly instead of lazy-requiring it as `any`. Keep the test seam
  but back it with a typed PdfTextExtractor function instead of the
  ad-hoc `any` shape.

Also pulls in the prettier reformat that `eslint --fix` produced across
the touched files and their specs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:57:22 -07:00
TehRiehlDeal a8a47e38c1 Add bulk statement-import feature for transactions
CI / secrets-scan (push) Successful in 8s
CI / sast (push) Successful in 13s
CI / vuln-scan (push) Successful in 15s
CI / test (push) Failing after 29s
CI / lint (push) Failing after 31s
CI / build-images (push) Has been skipped
CI / image-scan (push) Has been skipped
CI / push (push) Has been skipped
Lets a user upload a CSV, OFX/QFX, or PDF bank statement, parses the
transactions, flags duplicates and possible transfers against existing
records, and bulk-creates the accepted rows after a final read-only
confirmation step.

Backend
- New `statements` module with CSV (papaparse), OFX/QFX (node-ofx-parser),
  and PDF (pdf-parse) parsers behind a `StatementParser` strategy
  interface; format detected by content sniffing + extension.
- `DuplicateDetectorService` checks FITID/externalId exact matches first,
  then a date+amount+description Jaro-Winkler heuristic, then cross-account
  transfer pairing.
- New `POST /statements/parse` (multipart, in-memory, 10MB cap) returns the
  parsed preview without writing, including per-row `status` (`new`,
  `duplicate`, `needs_review`, `possible_transfer`) and any `needsMapping`
  payload when CSV headers are unrecognized.
- `POST /transactions/bulk` accepts up to 500 rows, chunks them 50 at a
  time inside `prisma.$transaction`, applies balance deltas, and writes a
  single `ActivityLog` row per chunk instead of one per transaction.
- Schema: nullable `external_id` column on `Transaction` plus composite
  indexes on `(account_id, external_id)` and `(account_id, date)` for fast
  dedupe-window queries. Not encrypted — it's an opaque bank ID used as a
  lookup key.

Frontend
- `ImportStatementDialog` runs a 4-step wizard: Upload → Column Mapping (if
  needed) → Review (editable table with duplicate/transfer badges) →
  Confirm (read-only summary with projected per-account balance impact and
  count-bearing primary button). The Confirm step gates the actual write,
  and Back to Review preserves all edit/checkbox state.
- New `bulkCreateTransactions` action on the transactions store.
- "Import Statement" button added next to Export on the Transactions page,
  with a success toast and a refresh of the transactions + accounts stores.

Tests
- 306 backend tests (29 suites), 195 frontend tests (31 suites), all green.
- Fixtures under `test/fixtures/statements/` cover three CSV sign
  conventions (signed-amount, debit/credit, credit-card), OFX 1.x SGML,
  OFX 2.x XML, and a credit-card QFX with the CCSTMTRS branch.

Versions bumped to 0.4.0 on both packages per the lockstep rule.

NOTE: the Prisma migration in
`prisma/migrations/20260527203542_add_transaction_external_id/` still
needs to be applied to the live database with `prisma migrate deploy` —
the DB at 10.0.3.82 wasn't reachable from the dev environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:23:45 -07:00
TehRiehlDeal a18858ea10 Pin pdfjs-dist to 5.4.296 to match react-pdf's bundled version
CI / test (push) Failing after 8s
CI / lint (push) Failing after 8s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 13s
CI / sast (push) Successful in 10s
CI / build-images (push) Has been skipped
CI / image-scan (push) Has been skipped
CI / push (push) Has been skipped
react-pdf@10.4.1 has an exact pdfjs-dist@5.4.296 dependency, but our
top-level pdfjs-dist resolved to 5.7.284, so the worker we shipped
mismatched the API code react-pdf called into and PDFs failed with:
"The API version 5.4.296 does not match the Worker version 5.7.284".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:30:34 -07:00
TehRiehlDeal 8e1bac5430 Bundle PDF.js worker via Vite ?worker so it ships as a .js asset
CI / vuln-scan (push) Successful in 14s
CI / sast (push) Successful in 10s
CI / test (push) Successful in 26s
CI / lint (push) Successful in 28s
CI / secrets-scan (push) Successful in 5s
CI / build-images (push) Successful in 1m57s
CI / image-scan (push) Successful in 50s
CI / push (push) Successful in 32s
Production deploy returned the worker module with
Content-Type: application/octet-stream — nginx's bundled mime.types
doesn't map .mjs and X-Content-Type-Options: nosniff stopped Firefox
from executing it, so PDFs failed with "Setting up fake worker failed".

Switch the worker import from ?url to ?worker and assign workerPort, so
Vite emits the worker as a regular hashed .js chunk that nginx already
serves correctly. Also add a nginx fallback that maps .mjs to
text/javascript for any future module assets, and include text/javascript
in gzip_types.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:21:35 -07:00
TehRiehlDeal c13b580bac Render PDF receipts inline with react-pdf instead of an iframe
CI / test (push) Successful in 29s
CI / lint (push) Failing after 25s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 14s
CI / sast (push) Successful in 10s
CI / build-images (push) Has been skipped
CI / image-scan (push) Has been skipped
CI / push (push) Has been skipped
Browsers' native PDF viewer refused to load blob-URL PDFs inside the
ReceiptViewer iframe (the "Click here to load" placeholder did nothing,
even though Open-in-new-tab worked). Replace the iframe with react-pdf
rendering on a canvas so we no longer depend on the in-iframe viewer:
fit-to-width Page, paginated nav for multi-page docs, selectable text
layer, and a Print button. The viewer is React.lazy-loaded so image-only
users don't download the pdfjs worker chunk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 23:05:27 -07:00
TehRiehlDeal 7adb2182fc Show transaction notes in expandable list rows
CI / test (push) Successful in 25s
CI / lint (push) Successful in 27s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 15s
CI / sast (push) Successful in 10s
CI / build-images (push) Successful in 1m59s
CI / image-scan (push) Successful in 50s
CI / push (push) Successful in 31s
Notes were write-only outside the edit form — visible nowhere in the
Transactions or Account Detail tables. Each row now has a chevron
toggle (alongside Edit/Delete) that reveals the notes in a second row,
mirroring the History page's expand pattern. Bumps both packages to
0.2.0 to keep frontend and backend in lockstep for the Harbor push.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:43:11 -07:00
TehRiehlDeal 4c84d2fb96 Bump Node 20 → 22 for native WebSocket support
CI / test (push) Successful in 31s
CI / lint (push) Successful in 27s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 13s
CI / sast (push) Successful in 9s
CI / build-images (push) Successful in 1m51s
CI / image-scan (push) Successful in 44s
CI / push (push) Successful in 32s
The deployed backend was crashing at startup with `Node.js 20 detected
without native WebSocket support` from @supabase/realtime-js. Native
WebSocket landed in Node 22.4 — bumping the base image is cleaner than
shimming `ws` as a transport (no extra dep, no constructor wrapper).

Bumped in three places to keep everything aligned:
- tehriehlbudget-backend/Dockerfile (runtime + build stages)
- tehriehlbudget-frontend/Dockerfile (build stage; nginx runtime
  unaffected)
- .gitea/workflows/ci.yml (test + lint jobs use the same Node)

@types/node is already on ^22.10.7, so no type-side changes needed.

Bump backend and frontend to 0.1.6 (frontend forced by per-service
push gate; no functional change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:19:35 -07:00
TehRiehlDeal dccc2940b9 Re-run prisma generate against the deploy tree
CI / test (push) Successful in 23s
CI / lint (push) Successful in 26s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 13s
CI / sast (push) Successful in 10s
CI / build-images (push) Successful in 1m51s
CI / image-scan (push) Successful in 43s
CI / push (push) Successful in 33s
The deployed backend was crashing with `Cannot read properties of
undefined (reading 'CREDIT')` on `client_1.AccountType.CREDIT`. The
.prisma/client/index.js inside the image was the unpopulated stub
that ships with @prisma/client; the schema-specific client (enums,
models, etc.) wasn't there.

Cause: pnpm deploy --prod pulls a fresh @prisma/client from the pnpm
store rather than copying the live, post-generate state of
/repo/node_modules/@prisma/client. The store copy ships with the stub.
Our previous `cp -r /repo/node_modules/.prisma` overlay attempted to
fix it but didn't land where node's resolver looks from /deploy.

Fix: re-run prisma generate against /deploy after pnpm deploy,
invoking the CLI from /repo (devDeps still present there) but with
cwd=/deploy so the generator writes into the deployed @prisma/client.
This produces a runnable image with the right enums/models in place.

Bump backend and frontend to 0.1.5 (frontend forced by per-service
push gate; no functional change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:09:34 -07:00
TehRiehlDeal 740591a791 Move @nestjs/config to runtime deps so the deployed image can find it
CI / test (push) Successful in 28s
CI / lint (push) Successful in 27s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 13s
CI / sast (push) Successful in 10s
CI / build-images (push) Successful in 1m49s
CI / image-scan (push) Successful in 43s
CI / push (push) Successful in 31s
app.module.ts imports @nestjs/config at runtime, but it was listed
under devDependencies. pnpm deploy --prod correctly stripped it,
producing a runnable-shaped image that crashes on boot with
`Cannot find module '@nestjs/config'`. Move it to dependencies and
let the lockfile reclassify accordingly.

Bump backend and frontend to 0.1.4. Frontend has no functional change
— forced bump to satisfy the per-service push gate in CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:58:00 -07:00
TehRiehlDeal bac97674a1 Make the deployed images actually run
CI / test (push) Successful in 25s
CI / lint (push) Successful in 27s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 13s
CI / sast (push) Successful in 11s
CI / build-images (push) Successful in 1m47s
CI / push (push) Successful in 30s
CI / image-scan (push) Successful in 43s
Two production-only failures the CI scan didn't catch:

Backend: the running container was crashing with `Cannot find module
'/app/dist/main'`. nest build was emitting `dist/src/main.js` instead
of `dist/main.js` because tsconfig.json had no rootDir, so tsc inferred
it as `.` and preserved the src/ subdirectory in the output. Set
`rootDir: "./src"` to flatten the output. Also exclude prisma/ from
tsconfig.build.json so prisma/seed.ts (a ts-node script that lives
outside src/) doesn't trip the rootDir check during builds.

Frontend: containers came up but were marked unhealthy because the
deployment's healthcheck targeted port 80 — which nginx-unprivileged
can't bind. Add a HEALTHCHECK directive to the image pointing at 8080
so any orchestrator inherits a working default. Compose-level
overrides still need to be updated independently.

Also clean up build-artifact gitignore patterns: *.tsbuildinfo and
compiled prisma/seed.* (a stale tsc invocation against the old
build config emitted them locally; they shouldn't ever be committed).

Bump backend and frontend to 0.1.3 — the broken 0.1.2 images are now
occupying those tags in Harbor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:45:41 -07:00
TehRiehlDeal 33484dc020 Fix backend image: include dist/ in the pnpm-deploy bundle
CI / test (push) Successful in 23s
CI / lint (push) Successful in 27s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 15s
CI / sast (push) Successful in 11s
CI / build-images (push) Successful in 1m48s
CI / push (push) Has been cancelled
CI / image-scan (push) Has been cancelled
The 0.1.1 image published with no dist/ at all and crashed on startup
(`Cannot find module '/app/dist/main'`). Cause: pnpm deploy uses pnpm
pack file selection, which honors .gitignore. The repo's root
.gitignore excludes dist/, so pnpm correctly omitted the compiled
JavaScript from the deploy bundle — but that's exactly what the
runtime needs.

Add an explicit `files` field to the backend's package.json listing
dist/ and prisma/. With `files` set, pnpm pack/deploy includes those
paths verbatim regardless of gitignore.

Bump 0.1.1 → 0.1.2; 0.1.1 is now occupied in Harbor by the broken
unrunnable image.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:26:09 -07:00
TehRiehlDeal b7394e7a32 Bump backend and frontend to 0.1.1 for the cosign-signed build
CI / vuln-scan (push) Successful in 12s
CI / test (push) Successful in 24s
CI / lint (push) Successful in 28s
CI / secrets-scan (push) Successful in 4s
CI / sast (push) Successful in 10s
CI / build-images (push) Successful in 1m48s
CI / image-scan (push) Successful in 45s
CI / push (push) Successful in 35s
Both packages were last published at 0.1.0 unsigned. The next push
attaches a cosign signature, which is a property of the published
artifact, so the version tag has to move forward — and the workflow's
own pre-push existence check would otherwise refuse to overwrite the
existing 0.1.0 digests in Harbor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:49:23 -07:00
TehRiehlDeal 857856fe4e Slim the backend image: pnpm deploy + drop bundled npm CLI
CI / test (push) Successful in 23s
CI / lint (push) Successful in 27s
CI / secrets-scan (push) Successful in 4s
CI / vuln-scan (push) Successful in 13s
CI / sast (push) Successful in 9s
CI / build-images (push) Successful in 1m46s
CI / image-scan (push) Failing after 43s
CI / push (push) Has been skipped
Trivy flagged 12 HIGH/CRITICAL CVEs on the backend image. 11 came from
/usr/local/lib/node_modules/npm — the npm CLI bundled with node:alpine,
which we never invoke (corepack/pnpm at build, `node dist/main` at
runtime). Delete it from the runtime stage; that alone clears 11
findings (cross-spawn RegEx DoS, multiple node-tar arbitrary-write
CVEs, minimatch DoS, glob command injection).

The 12th was serialize-javascript@6.0.2, pulled in via
@rollup/plugin-terser (used by vite-plugin-pwa on the frontend). It was
landing in the backend image because the previous Dockerfile relied on
`pnpm install --filter backend... --prod` over a hoisted workspace,
which still installs every workspace package's transitive deps in the
shared root node_modules. The runtime image was shipping vite, vitest,
react, the whole NestJS CLI — none of which it needs.

Switch to `pnpm deploy --prod --filter tehriehlbudget-backend /deploy`,
which produces a self-contained, prod-only, hoisted bundle for just the
backend. Copy the generated Prisma client into the deploy explicitly
since .prisma/client/ is a co-located output directory, not a package,
and pnpm deploy doesn't always carry it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:26:15 -07:00
TehRiehlDeal b2bc6ba9ca Fix backend image build: use hoisted node_modules in the Docker context
CI / test (push) Successful in 24s
CI / lint (push) Successful in 27s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 12s
CI / sast (push) Successful in 10s
CI / build-images (push) Successful in 2m20s
CI / image-scan (push) Failing after 44s
CI / push (push) Has been skipped
The previous Dockerfile assumed Prisma's generated client landed at
tehriehlbudget-backend/node_modules/.prisma/client/, but pnpm's default
isolated layout writes it inside node_modules/.pnpm/@prisma+client@.../
node_modules/, which doesn't survive a multi-stage COPY. The build
failed at the runtime stage trying to copy a path that didn't exist.

Switch the image to hoisted (flat) node_modules via a build-context-only
.npmrc so prisma generate writes to predictable /repo/node_modules/
{@prisma,.prisma}/ paths. Local dev keeps the isolated layout — the
.npmrc lives only inside the docker build context, not on disk.

The runtime stage now copies node_modules from the workspace root
(where hoisted deps live), then overlays the generated Prisma client
from the build stage (since the prod-deps stage strips the prisma CLI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:06:52 -07:00
TehRiehlDeal 8c10124272 Build, scan, and push images to Harbor on every main push
CI / test (push) Successful in 25s
CI / lint (push) Successful in 28s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 12s
CI / sast (push) Successful in 10s
CI / build-images (push) Failing after 51s
CI / image-scan (push) Has been skipped
CI / push (push) Has been skipped
Wires up the CD half of the pipeline. New jobs build multi-stage Docker
images for the frontend and backend, run a Trivy image scan that fails
on HIGH/CRITICAL findings, and push to harbor.tehriehldeal.com on main
only. Each push tags <version> (from package.json), <sha>, and latest;
a pre-push existence check refuses to overwrite a version tag that
already points at a different digest, forcing a real bump.

The Vite frontend now reads runtime config from window.__RUNTIME_CONFIG__,
populated by /config.js which nginx renders from container env vars at
startup via envsubst. A getConfig() helper falls back to import.meta.env
for `pnpm dev` and Vitest, so existing test scaffolding keeps working.
PWA workbox excludes /config.js from precache and serves it NetworkOnly
to keep stale config from surviving a container restart.

Bumps frontend 0.0.0→0.1.0 and backend 0.0.1→0.1.0 (production
deployment is a meaningful new capability for both packages).

Also fixes four pre-existing tsc -b errors that the new vite build step
in the frontend Dockerfile would otherwise hit: global.fetch →
globalThis.fetch in three test files, null-guard in Activity.tsx
account filter, type cast on Recharts Pie onClick in Dashboard.tsx,
typed callback signature on the auth.test.ts onAuthStateChange mock.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:49:01 -07:00
TehRiehlDeal a4ee21f8c2 Make the lint job pass
CI / test (push) Successful in 27s
CI / lint (push) Failing after 29s
CI / secrets-scan (push) Successful in 5s
CI / vuln-scan (push) Successful in 12s
CI / sast (push) Successful in 10s
Three categories of change, all required for `pnpm lint` and
`pnpm format:check` to exit clean:

Type-safety fixes in backend production code:
- Add Express type augmentation for `Request.user` so AuthGuard,
  CurrentUser decorator, and EncryptionInterceptor can drop their
  `any`-typed `getRequest()` calls
- Replace `data: any` patterns in AccountsService, TransactionsService,
  and ActivityLogService with proper `Prisma.*UncheckedCreateInput` /
  `Prisma.*UncheckedUpdateInput` / `Prisma.DateTimeFilter` types
- Type AdvisorService's `stripPII` recursion as `unknown`-narrowing
  and the Ollama fetch response as a structured shape
- Type SupabaseService's client via `ReturnType<typeof createClient>`
  to side-step the SupabaseClient generic-arity mismatch
- Type the snapshot/summary helpers' Decimal fields as
  `Prisma.Decimal | number | string` instead of `any`
- Mark `bootstrap()` in main.ts as `void`-prefixed

Type-safety fixes in frontend production code:
- Type `(v: any)` SelectValue render callbacks as `string | undefined`
  across TransactionForm, Transactions, Activity, Accounts
- Type form submit handlers in Transactions and AccountDetail with
  the existing `TransactionFormData` interface
- Type the Recharts onClick entry in Dashboard

ESLint config tuning:
- Backend: relax the `no-unsafe-*`, `require-await`, `unbound-method`,
  and `no-unused-vars` rules for `*.spec.ts` files only — Jest mocks
  cannot satisfy strict typing without disproportionate ceremony
- Frontend: ignore `coverage/`, relax `no-explicit-any` in test
  files, demote `react-refresh/only-export-components` to warning
  inside `components/ui/` (shadcn intentionally co-locates `cva`
  variants with components), demote `react-hooks/set-state-in-effect`
  to warning across the project (5 legitimate-but-suboptimal patterns
  that need component-level refactoring)

Tooling:
- Add prettier as a root workspace devDependency so `pnpm format:check`
  resolves the binary
- Run `pnpm format` once to baseline the codebase against the
  configured prettier ruleset (singleQuote, trailingComma, printWidth
  100, tabWidth 2)

Backend tests: 213/213 still pass. Frontend tests: 170/170 still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:20:23 -07:00
TehRiehlDeal 0bd90d1fa0 Cover the remaining backend service branches
Pushes backend branch coverage from 80 to 87 by exercising the
previously-unhit paths: transactions service notes encryption on
create/update, source==destination validation on create and update,
not-found and TRANSFER-conversion branches on update, the date-range
sub-branches on findAll, and findOne; the advisor's flat / spending-up
trend variants, the no-prior-period fallback, the zero-income savings
rate, the empty-categories placeholder, and the no-env-var ollama
defaults; aggregations.getSummary end-to-end; the date-input fallback
and default 365-day window in valuations.list; the encryption
interceptor's paginated-response and primitive-passthrough paths plus
its no-body and no-matching-fields request paths; the files service's
existing-userdir and no-extname paths plus the UPLOAD_DIR fallback;
and the activity-log service's open-ended date-range filters.

The residual ~3 percent gap to 90 is almost entirely
ts-jest decorator-metadata branches on controllers and DTOs, which
aren't real code paths and can't be tested away without swapping
coverage providers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:02:23 -07:00
TehRiehlDeal 6cd785bfcf Raise test coverage on stores, lib utilities, and the activity-log controller
Tests / test (push) Successful in 23s
Excludes pure-wiring files from coverage measurement (backend NestJS
modules and main.ts; frontend ShadCN UI primitives, the test harness,
*.d.ts files, and config files) so the numbers reflect actual business
logic rather than DI boilerplate. Adds the missing
activity-log.controller spec, fills the encryption service round-trip
branch, and lifts the frontend stores from ~70% to ~99% by covering
auth.initialize and onAuthStateChange, optimistic reorderAccounts,
transactions.updateTransaction and fetchAllTransactions,
advisor.startConversation/sendMessage error and period-forwarding paths,
aggregations.fetchCashFlow, and the previously-untested activity store.
Also new lib tests for the auth-aware fetch wrapper, date helpers, and
account-type predicate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:56:35 -07:00
TehRiehlDeal 4b0fcb7df0 Tie the AI advisor to the dashboard's selected date range
Tests / test (push) Successful in 23s
Previously the advisor always analyzed the current calendar month and
compared it against the previous calendar month, regardless of which
range the user had selected on the dashboard. That meant clicking
"Last 90 days" updated the cards but the advice was still scoped to
this month — the two surfaces disagreed on what "now" meant.

The chat payload now carries an optional period { startDate, endDate,
label } that the dashboard derives from its existing range state. When
present, the service uses that window as the current period and the
equal-length window immediately before it as the comparison period
(so "Last 30 days" pairs naturally with the prior 30 days). Period
labels flow into the prompt sections so the model talks about the
window the user is actually looking at. The legacy /advisor/insights
GET endpoint and any caller that omits the period keep the original
this-month / last-month behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:39:51 -07:00
TehRiehlDeal ba08eafd6a Stop the advisor from sneaking parenthetical math into its replies
Tests / test (push) Successful in 22s
Smaller local models still tacked equations like "($749.51 / 0.267)"
onto sentences despite the prompt forbidding it — and they often got
the arithmetic wrong on top of inventing the equation. Two layers of
defense: hoist the no-math rule to the top of the system prompt under
a CRITICAL header with concrete forbidden examples, and strip any
parenthetical containing both a dollar amount and an arithmetic
operator from the model's reply before it leaves the service. Plain
parentheticals like "(see below)" or "($450 this month)" pass through
untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:25:00 -07:00
TehRiehlDeal 0aa2daaee4 Fix dashboard range filter dropping startDate/endDate query params
The aggregations controller typed its query DTO as a `class` with no
class-validator decorators. Because main.ts installs ValidationPipe with
`whitelist: true`, every undecorated property was stripped before
reaching the handler, so all three aggregations endpoints silently fell
back to "current month UTC" no matter what the URL said. Switching to
an `interface` (matching the convention TransactionFilters and
ActivityLogFilters already use) takes the DTO out of class-validator's
metatype reflection and lets the dates through untouched.

Adds a regression test that exercises the real HTTP pipeline via
supertest with the same useGlobalPipes config as main.ts — the existing
direct-call controller tests bypass the pipeline, which is how the bug
shipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:41:14 -07:00
TehRiehlDeal 4939122cf2 Stop the advisor from inventing bogus savings equations
The model was producing lines like "you've saved $755.21 ($18,952.39 -
$83,276.19)" — pulling net worth and total debt from the snapshot and
subtracting them as if that yielded monthly savings. The figures had no
relationship to each other, and the parenthetical math didn't even
equate to the dollar number it cited.

Two fixes in the system prompt:
- Pre-compute "Saved this month" = income - expense (and the same for
  last month) so the model never has to derive it.
- Section the numbers into "Standing balance (point-in-time)" and "This
  month (flow)" with an explicit rule that standing-balance figures are
  not flow and must not be subtracted from each other.
- Add a guardrail forbidding invented equations and parenthetical
  arithmetic in the narrative.

Also formats every dollar figure with thousands separators so the model
sees and echoes "$15,000.00" instead of "$15000".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:34:13 -07:00
TehRiehlDeal 4b3b1c71f9 Record activity history and support unpaginated transaction fetch
Adds an ActivityLog model that captures create/update/delete actions on
transactions, accounts, and valuations, with snapshots that survive a
cascading account deletion so a deleted transaction's amount and
description remain auditable. Each service writes its log entry inside
the same $transaction as the underlying mutation, so a failed mutation
rolls back the log too.

Also extends GET /transactions with all=true to skip pagination (capped
at 10,000 rows) so the upcoming CSV export can pull every matching row
in one request.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 13:53:22 -07:00
TehRiehlDeal e5fe8568b0 Reverse counter-party balances when deleting an account
Prisma cascade on destinationAccountId silently wiped transfer rows
without running TransactionsService.remove(), leaving the surviving
account with a balance that reflected a transfer that no longer
existed. AccountsService.remove() now iterates related transfers and
reverses the counter-party delta inside the same $transaction before
deleting the account.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:31:10 -07:00
TehRiehlDeal e5bc486e77 Space balance chart points equidistantly instead of by real time
The previous fix gave each point a unique x-axis timestamp so the tooltip
could disambiguate same-day entries, but that caused real-time-scale
spacing: transactions clustered minutes apart became impossible to click
on individually. The desired behavior is equidistant slots for display,
with the x value only needing to be unique for tooltip hit-testing.

Switches the balance chart to a categorical x-axis keyed on the point's
array index (computed on the frontend after fetching). The tick formatter
looks up the date from the history array at that index so labels still
show the calendar date. Each point occupies its own evenly-spaced slot
and hover correctly identifies the specific point. The backend's now-
unused `timestamp` field is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:51:43 -07:00
TehRiehlDeal 4f918fc14a Give each balance chart point a unique x-axis timestamp
Recharts picks one payload entry per x-axis value for its tooltip, so
when multiple transactions shared the same YYYY-MM-DD date, hovering any
of them surfaced only the first one's description — making the new
transaction-context tooltip misleading for same-day entries.

Each point now carries a millisecond `timestamp` (from the transaction's
createdAt for transaction-driven charts, or the valuation's date for
market-value charts, plus the request time for the "today" bookend).
The balance chart x-axis switches to a numeric time scale keyed on
timestamp, so same-day points sit at distinct x positions and hover
correctly identifies the specific point. The tooltip reads the date
from the point's payload instead of the x-axis label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:44:58 -07:00
TehRiehlDeal b6a3dba50e Theme chart tooltips and enrich balance chart with transaction context
Replaces the default white Recharts tooltip (which was jarring in dark
mode) with a themed ChartTooltip component that uses the popover /
foreground / muted-foreground tokens so it adapts to light and dark
themes. Applied to the Spending, Income vs Expense, Cash Flow, and
Balance over time charts for visual consistency.

The Balance over time tooltip now also includes, when the point
corresponds to a transaction, the transaction's description and the
signed balance change (green for inflow, red for outflow). The backend
endpoint was extended to return `description` and `change` alongside
each transaction point.

Also kills the mobile tap-highlight box and focus outline that Recharts
SVG elements render on tap — it was drawing a visible rectangle around
the hit region on touch devices. A short rule in index.css scoped to
.recharts-wrapper / .recharts-surface zeroes both.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:37:16 -07:00
TehRiehlDeal 89e7d7c647 Fix same-day ordering and off-by-one labels on balance chart
Two related bugs:

1. Same-day transactions were walked in arbitrary order. The balance-history
   endpoint ordered by `date desc` only; since all same-day transactions
   share a noon-UTC timestamp, Prisma returned them in an undefined order
   and the reverse walk could place income after expenses, making the chart
   dip below the pre-income balance. Adds `createdAt desc` as a secondary
   sort so the walk processes newest-inserted transactions first, which
   produces an oldest-first chart sequence that matches the order the user
   entered them.

2. Chart tick labels and tooltip headers were off by one day. The XAxis
   tickFormatter and Tooltip labelFormatter called `new Date("YYYY-MM-DD")`,
   which parses as midnight UTC — in timezones west of UTC this renders as
   the previous day. Swaps to local-component parsing (tick) and the
   existing `formatDate` helper (tooltip) so labels always match the stored
   calendar date.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:26:39 -07:00
TehRiehlDeal 69c9c1aecc Fix dashboard missing transactions on the end date of the range
Transactions are stored at noon UTC (per parseDateInput) so the calendar
date survives timezone shifts, but parseDateRange was constructing the
end boundary via `new Date("YYYY-MM-DD")`, which parses as midnight UTC.
A transaction dated "2026-04-30" (2026-04-30T12:00:00Z) therefore fell
AFTER the filter's `lte` boundary and was silently excluded from the
income/expense, spending-by-category, and cash-flow aggregations for any
range ending on that day.

Rebuilds parseDateRange to explicitly use UTC start-of-day for `gte` and
UTC end-of-day for `lte`, so any transaction on the endpoint day is now
included. Adds a regression test that asserts the Prisma filter range
covers noon UTC on the last day of the range.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 10:09:59 -07:00
TehRiehlDeal 3df88154e2 Fix blank advisor reply by priming a user turn on empty chat
Chat models (Ollama /api/chat) don't generate a reply when given only a
system message — they need a user turn to respond to. The new conversational
flow sent an empty messages array on "Get Advice", so Ollama received
only [system] and returned empty content, which rendered as a blank pill
in the UI. When the client message list is empty, inject a synthetic
opening-prompt user turn so the model always has something to reply to.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 18:53:51 -07:00
TehRiehlDeal 5e04bee05e Make AI advisor conversational and chat-capable
Fixes the "undefined category" bug in generated insights: the PII-strip
filter listed 'name' as sensitive, which wiped category labels like
"Groceries" out of the prompt before it reached the model. Category
names are user-defined labels, not PII, so they're removed from the
filter; a defensive `?? 'Uncategorized'` fallback guards against future
regressions.

The system prompt is rewritten as a friendly first-person persona that
grounds its replies in exact dollar amounts and a month-over-month
comparison (income, expenses, savings rate, top-3 categories for both
months) instead of the previous generic "3-5 numbered tips" format.

Adds a stateless POST /advisor/chat endpoint that accepts the full
client-held message history and replies with the next assistant turn,
rebuilding the financial context on every call. The dashboard card
becomes a mini-chat: user/assistant bubbles, follow-up input, and a
restart button, with auto-scroll to the latest message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 17:54:44 -07:00
TehRiehlDeal b5647dca11 Add cash-flow card and range selector to dashboard
Adds a new /aggregations/cash-flow endpoint that reports money entering
and leaving liquid accounts (CHECKING/SAVINGS/CASH) over a period.
Defining outflows as "reduces a liquid account" naturally counts
credit-card and loan payments as outflows while excluding the original
credit-card swipes (which don't touch cash until paid), avoiding
double-counting. Dashboard gains a shared range selector (This month,
Last 30d, Last 90d, YTD) controlling the summary cards, spending, and
the new cash-flow card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 07:42:29 -07:00
TehRiehlDeal fdcbe2611d Add CASH/INVESTMENT/RETIREMENT account types with market-value tracking
Extends AccountType enum and counts all three new types as assets in net
worth. For STOCK/INVESTMENT/RETIREMENT accounts, adds an AccountValuation
model and /accounts/:id/valuations endpoints so users can log periodic
balance snapshots; the balance-history chart uses those snapshots for
market-value accounts instead of reconstructing from transactions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-16 22:05:11 -07:00
TehRiehlDeal d04686c1da Fix off-by-one date bug by parsing/displaying dates timezone-independently 2026-04-16 16:17:07 -07:00
TehRiehlDeal bc599ebb5a Auto-update account balances for all transaction types (INCOME/EXPENSE/TRANSFER) 2026-04-16 16:09:27 -07:00
TehRiehlDeal 78c337cfe1 Add balance-over-time chart to account detail page 2026-04-16 16:03:04 -07:00
TehRiehlDeal 9cb483a733 Add receiptPath to transaction DTO so it isn't stripped by ValidationPipe 2026-04-16 15:37:08 -07:00
TehRiehlDeal 6bda259e99 Fix net worth to subtract liabilities instead of summing all balances 2026-04-16 14:43:16 -07:00
TehRiehlDeal ddf5c544c1 Add clickable account details page and drag-and-drop reordering 2026-04-16 14:37:10 -07:00
TehRiehlDeal 172845dc4d Invert transfer balance direction for liability accounts (credit, loan) 2026-04-16 14:19:53 -07:00
TehRiehlDeal e63eed9d90 Add transfer support: source → destination accounts with atomic balance updates 2026-04-16 14:11:30 -07:00
TehRiehlDeal 1da33a845d Fix query param type coercion and default date ranges for aggregations 2026-04-12 21:24:05 -07:00
TehRiehlDeal e16acc6950 Enable CORS and add /api global route prefix 2026-04-12 21:18:54 -07:00
TehRiehlDeal 026351b5a4 Downgrade Prisma to v6 for Docker compatibility 2026-04-12 21:13:31 -07:00
TehRiehlDeal 43d6a18aba Fix production build errors (type imports, Prisma constructor, TS6 compat, Recharts types) 2026-04-12 20:59:27 -07:00
TehRiehlDeal 37f8e2e7c3 Replace Anthropic API with local Ollama for AI advisor 2026-04-12 17:36:34 -07:00
TehRiehlDeal 17de171d26 Remove Plaid integration 2026-04-12 17:13:33 -07:00
TehRiehlDeal 6987a50474 Add AI financial advisor with Claude and PII stripping 2026-04-12 17:03:06 -07:00
TehRiehlDeal 8c844e0122 Add Plaid integration for external account linking 2026-04-12 17:03:06 -07:00