diff --git a/codebase-review/01-architect.md b/codebase-review/01-architect.md
new file mode 100644
index 0000000..f3977ec
--- /dev/null
+++ b/codebase-review/01-architect.md
@@ -0,0 +1,211 @@
+# Architect Review — WealthySmart
+
+> Hat: Staff-level Software Architect. Focus: structure, layering, data model, migration story, API consistency, deploy topology.
+>
+> **Editorial note:** ARCH-01 was corrected after verification — see the finding itself and `README.md`.
+
+## Executive Summary
+
+WealthySmart is a single-user personal finance management application with a modern full-stack architecture: FastAPI + SQLModel backend (PostgreSQL) paired with a React 19 + Vite frontend served through a Hono proxy, plus an LLM-powered AI agent interface. The system handles multi-currency transactions (CRC/USD/EUR/BTC/XMR), budget forecasting with complex billing-cycle logic, pension/municipal bill tracking, and push notifications. The architecture is sound overall — no fundamental redesign is needed — but there are weak defaults in configuration, zero test coverage, an ad-hoc migration story, and consistency gaps in validation and response schemas that should be addressed incrementally.
+
+## How the System Fits Together
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ DEPLOYMENT (Docker Compose) │
+├─────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌──────────────────────┐ ┌─────────────────────┐ │
+│ │ Frontend (Node) │ │ Backend (FastAPI) │ │
+│ │ ┌─────────────────┐ │ │ ┌───────────────┐ │ │
+│ │ │ Vite (port 3000)│ │ │ │ Uvicorn │ │ │
+│ │ │ React 19 + TS │ │ │ │ (port 8000) │ │ │
+│ │ └─────────────────┘ │ │ └───────────────┘ │ │
+│ │ ┌─────────────────┐ │ │ ┌───────────────┐ │ │
+│ │ │ Hono Server │ │ │ │ 15 endpoints │ │ │
+│ │ │ (port 3001) │ │ │ │ + LLM agent │ │ │
+│ │ │ • SPA serving │ │ │ └───────────────┘ │ │
+│ │ │ • CopilotKit │ │ │ │ │
+│ │ │ runtime │ │ │ │ │
+│ │ │ • /api proxy │ │ │ │ │
+│ │ └─────────────────┘ │ │ │ │
+│ └──────────────────────┘ └─────────────────────┘ │
+│ │ │ │
+│ └────────────────────────────┘ │
+│ │ │
+│ ┌──────────────────────────────────┐ │
+│ │ PostgreSQL 16 (port 5432) │ │
+│ │ • 15 tables + enums │ │
+│ │ • No Alembic migrations │ │
+│ └──────────────────────────────────┘ │
+│ │
+│ Production: nginx-proxy + Let's Encrypt on single VPS │
+│ Ingestion: n8n flows POST to /api/v1 with API tokens │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+**Key data flows:**
+- Frontend requests `/api/v1/...` (proxied by Hono to FastAPI)
+- Agent requests go to `/api/v1/agent/agui` (Microsoft Agent Framework + OpenAI)
+- Exchange rates refreshed every 6 hours from multiple fallback APIs
+- Auth via httpOnly cookies (SPA) or Bearer tokens (API clients / n8n)
+
+## Strengths
+
+1. **Modular endpoint structure** — 15 focused routers (accounts, transactions, budget, analytics, etc.) with clear single responsibilities.
+2. **Sophisticated budget projection logic** — `budget_projection.py` correctly handles billing cycles (18th–18th for credit cards vs. 1st–1st calendar month), deferred transactions, recurring items with override amounts, and cumulative balance tracking across years, with a clean API (`compute_monthly_projection`, `compute_yearly_projection_with_cumulative`).
+3. **Resilient exchange-rate handling** — fallback chain (BCCR → ExchangeRate-API → currency-api → FloatRates → CoinGecko) with in-memory and database caching; uses a `_last_known_*` pattern to retain previous values during API outages.
+4. **Clean auth model** — dual-mode authentication (httpOnly cookie for SPA, Bearer token for API clients) with JWT + API-token support. Token revocation marks rows inactive rather than deleting them.
+5. **Seed data for development** — comprehensive default categories, accounts, and recurring items; single-user design appropriate for the app.
+6. **Type-safe models** — SQLModel schemas enforce some constraints (unique categories, year/month combinations) at the DB level.
+7. **REST conventions mostly respected** — POSTs return 201, pagination via `offset`/`limit` is consistently applied.
+8. **Consistent error surface** — `HTTPException` with clear status codes and detail messages throughout.
+
+## Findings
+
+### [ARCH-01] Weak secret hygiene around `.env` files and config defaults
+**Severity:** High (downgraded from Critical after verification)
+**Location:** `.env`, `.env.prod` (working tree), `backend/app/config.py:6–9`, `docker-compose.yml`
+**Evidence (corrected):** `.env` and `.env.prod` exist in the repo root and contain real secrets (OpenAI API key, DB password, admin credentials, VAPID keys), but **they are gitignored and were never committed** — `git ls-files` and `git log --all --diff-filter=A` confirm no history. The genuine issues are: (a) `config.py` ships working fallback credentials (`SECRET_KEY="change-me-in-production"`, `ADMIN_USERNAME/PASSWORD = "admin"`), (b) `docker-compose.yml` hardcodes dev DB credentials, and (c) plaintext secrets on the dev machine with no rotation discipline.
+**Why it matters:** If a deploy ever runs without its env file (typo in compose, CI regression — note `reference_gitea_deploy`: `.env.prod` is regenerated each deploy), the app silently boots with `admin/admin` and a known JWT signing key.
+**Recommendation:** Remove the defaults and fail fast at startup (see SEC-01). Keep `.env.example` as the documented template. Rotate any key that has been pasted into chats/logs.
+
+### [ARCH-02] No Formal Database Migration Strategy
+**Severity:** High
+**Location:** `backend/app/db.py:13–76`; no `alembic/` directory
+**Evidence:** Migrations are ad-hoc SQL strings in `run_migrations()` with `IF NOT EXISTS` clauses. Adding a column requires hand-written `ALTER TABLE` code. Schema evolution is not version-controlled or reversible.
+**Why it matters:** Manual migrations are error-prone and have no rollback. Dev and prod can diverge silently. The project rule "never reset prod DB; use migrations" makes this the single most important infrastructure gap.
+**Recommendation:** Introduce Alembic: `alembic init`, autogenerate a baseline from current models, convert the existing manual statements, run `alembic upgrade head` at container startup. Effort: **M** (1–2 days).
+
+### [ARCH-03] Validation & Error-Handling Inconsistencies
+**Severity:** Medium
+**Location:** `endpoints/transactions.py:71–90`, `endpoints/budget.py:108–109`, `endpoints/analytics.py:56`
+**Evidence:** `list_transactions()` mixes `cycle_year/cycle_month` and `start_date/end_date` filters with `or_` in a nested query — unclear precedence. `budget.py` uses positional `HTTPException(400, ...)` while others use keyword args. `analytics.py:56` filters with the string literal `"COMPRA"` instead of `TransactionType.COMPRA`.
+**Why it matters:** Mixing filter modes risks undefined behavior; hardcoded enum strings break refactoring safety; inconsistent error style hampers debugging.
+**Recommendation:** Extract a `build_transaction_query()` helper with explicit precedence; standardize on `status_code=` kwargs; replace string literals with enum members. Effort: **S**.
+
+### [ARCH-04] Agent Tools Duplicate Service Logic
+**Severity:** Medium
+**Location:** `backend/app/agent/tools.py`, `backend/app/services/budget_projection.py`
+**Evidence:** `tools.py` re-implements queries (`get_accounts()`, `get_net_worth()`, `get_cycle_summary()`) that overlap with the service layer instead of delegating to it.
+**Why it matters:** Budget logic changes must be applied in two places or the assistant and the UI disagree about the user's money.
+**Recommendation:** Make agent tools thin wrappers over service functions. Effort: **M** (1 day).
+
+### [ARCH-05] No Input Limits on Bulk Operations
+**Severity:** Medium
+**Location:** `endpoints/import_transactions.py:96–149`
+**Evidence:** `paste_import()` accepts arbitrary text with no size cap; a 10 MB paste is parsed entirely in memory before any validation.
+**Why it matters:** Memory-exhaustion DoS vector; bulk operations should validate request size upfront.
+**Recommendation:** `Field(..., max_length=1_000_000)` on the request model; pre-validate before opening the write loop. Effort: **S**.
+
+### [ARCH-06] Missing Test Coverage
+**Severity:** High
+**Location:** No `tests/` directory anywhere; `requirements.txt` lacks pytest
+**Evidence:** Zero test files in the repository.
+**Why it matters:** Budget projection is the most intricate logic in the app (billing-cycle edges, deferred carryover, cumulative balances with overrides) and is entirely unguarded against regression.
+**Recommendation:** `backend/tests/` with `test_budget_projection.py` (Feb 28/29, year boundaries, overrides), `test_auth.py`, `test_exchange_rate.py`; Vitest for `useBudget`. Effort: **L** (3–5 days for meaningful coverage).
+
+### [ARCH-07] Single Hono Instance Is a Single Point of Failure
+**Severity:** Low (revised for a single-user app)
+**Location:** `frontend/server.ts:1–20`, `frontend/Dockerfile`, `docker-compose.prod.yml`
+**Evidence:** One Hono container serves SPA + CopilotKit runtime + proxy. The Dockerfile healthcheck probes the backend's `/api/health`, not Hono's own readiness.
+**Why it matters:** If Hono crashes the whole app is down even with a healthy backend. For a single-user app, replicas are over-engineering; a correct healthcheck + restart policy is the right-sized fix.
+**Recommendation:** Healthcheck Hono itself; `restart: unless-stopped`; add 5xx retry in the frontend API client. Skip load balancing.
+
+### [ARCH-08] Exchange-Rate Refresh Loop Swallows Cancellation
+**Severity:** Low
+**Location:** `backend/app/services/exchange_rate.py:348–366`, `backend/app/main.py:66`
+**Evidence:** `refresh_rates_periodically()` runs `while True` with a broad except that logs and continues; cancellation during shutdown can be silently swallowed.
+**Recommendation:** Catch `asyncio.CancelledError` explicitly and `break`; keep the broad handler for everything else. Effort: **S**.
+
+### [ARCH-09] No Foreign-Key Delete Rules
+**Severity:** Medium
+**Location:** `backend/app/models/models.py:144, 258, 427`
+**Evidence:** `Transaction.category_id` and similar FKs declare `foreign_key=` with no `ondelete` behavior; deleting a category leaves dangling references.
+**Why it matters:** Silent referential inconsistency — budget rows pointing at categories that no longer exist.
+**Recommendation:** Decide per-FK between `SET NULL` (transactions should outlive categories) and `CASCADE` (water readings die with their receipt); apply via Alembic migration. Effort: **S**.
+
+### [ARCH-10] Fragile Cookie Parsing in Agent Middleware
+**Severity:** Medium
+**Location:** `backend/app/main.py:88–142`
+**Evidence:** Cookie extraction uses regex `r"(?:^|;\s*)ws_token=([^;]+)"`, which mishandles edge cases (multiple cookies of the same name, unusual encodings).
+**Recommendation:** Use stdlib `http.cookies.SimpleCookie`. Effort: **S**.
+
+### [ARCH-11] `deferred_to_next_cycle` Implemented in Backend but Not Exposed in UI
+**Severity:** Medium
+**Location:** `backend/app/models/models.py:145`, `endpoints/transactions.py:219–235`
+**Evidence:** The field is settable via PATCH and respected by budget projection; the deferral toggle exists in row actions but `TransactionModal.tsx` has no field for it, and the affordance is undiscoverable (see UX-05).
+**Recommendation:** Add the checkbox to `TransactionModal.tsx` and make the row-action state visible. Effort: **S**.
+
+### [ARCH-12] Pagination Lacks a Deterministic Tiebreaker
+**Severity:** Low
+**Location:** `endpoints/transactions.py:96`, `endpoints/salarios.py:35–36`
+**Evidence:** Paginated queries order by `date DESC` only; equal dates yield non-deterministic page boundaries.
+**Recommendation:** `.order_by(col(...).date.desc(), col(...).id.desc())` everywhere paginated. Effort: **S**.
+
+### [ARCH-13] Mutation → Refetch-Everything Pattern
+**Severity:** Low
+**Location:** `frontend/src/hooks/useBudget.ts:61–74`
+**Evidence:** Every mutation triggers `Promise.all([fetchProjection(), fetchMonthDetail(), fetchRecurringItems()])` regardless of what changed.
+**Why it matters:** Wasteful, and the pattern will multiply as modules grow. Acceptable as a simplicity tradeoff today; the durable fix is a query library (see FE-10 / Plan Phase 3).
+
+### [ARCH-14] Hardcoded Dev DB Credentials in Compose
+**Severity:** Medium
+**Location:** `docker-compose.yml` (db service `environment:` block)
+**Evidence:** `POSTGRES_PASSWORD: wealthy_pass` hardcoded in source control (prod compose correctly uses `${POSTGRES_PASSWORD}`).
+**Recommendation:** Move dev credentials to the gitignored `.env`; reference via `${...}` in both compose files. Effort: **S**.
+
+### [ARCH-15] N+1 Query in Category Aggregation
+**Severity:** Medium
+**Location:** `backend/app/services/budget_projection.py:370–383`
+**Evidence:** `compute_cc_by_category()` calls `session.get(Category, cat_id)` inside a loop — one query per category with spending.
+**Recommendation:** Prefetch `{c.id: c for c in session.exec(select(Category)).all()}` and look up in the dict. Effort: **S** (minutes).
+
+### [ARCH-16] API Response Schema Inconsistency
+**Severity:** Low
+**Location:** `endpoints/transactions.py:28–33` (Pydantic models) vs `endpoints/budget.py:100–157` (raw dicts) vs `endpoints/analytics.py:18–23`
+**Evidence:** Some endpoints return typed models, others plain dicts; OpenAPI schema generation is incomplete and frontend types are maintained by hand.
+**Recommendation:** Shared models in `app/api/schemas.py` + `response_model=` on every route; this also unlocks frontend type codegen (see FE-14). Effort: **M**.
+
+### [ARCH-17] Agent Session via ContextVar Couples Tools to HTTP Middleware
+**Severity:** Low
+**Location:** `backend/app/agent/tools.py:40–52`, `backend/app/main.py:131–141`
+**Evidence:** Tools fetch the DB session from a `contextvars.ContextVar` set by middleware; calling a tool outside a request raises `LookupError`.
+**Recommendation:** Document the constraint and add a clear error message (see BE-12); full refactor only if tools ever need to run outside HTTP. Effort: **S** for the guard.
+
+### [ARCH-18] No Data Export / Archival Path
+**Severity:** Low
+**Location:** All tables are append-only with no export endpoint
+**Why it matters:** Acceptable growth for a personal app, but there is no way to get historical data out (taxes, accountant, backup verification).
+**Recommendation:** CSV export endpoint per major table (also UX wishlist #5). Effort: **M**.
+
+### [ARCH-19] Hono Proxy Trusts Upstream Blindly
+**Severity:** Low
+**Location:** `frontend/server.ts` proxy handlers
+**Evidence:** Responses are piped through without shape validation; combined with FE-14 (no runtime validation client-side), schema drift fails confusingly.
+**Recommendation:** Don't validate in the proxy (wrong layer); fix via shared response schemas (ARCH-16) and optional Zod at the API-client boundary.
+
+### [ARCH-20] Lazy Imports Papering Over a Circular Dependency
+**Severity:** Low
+**Location:** `backend/app/auth.py:39–43`
+**Evidence:** `_validate_token()` imports `get_session` and `APIToken` inside the function body to dodge a cycle with `db.py`.
+**Recommendation:** Split token utilities into their own module to break the cycle properly. Effort: **S**.
+
+## Recommended Refactors (prioritized)
+
+| # | Item | Effort | Phase |
+|---|---|---|---|
+| 1 | ARCH-01/SEC-01: fail-fast on default secrets, fix dev compose creds | S | 0 |
+| 2 | ARCH-02: Alembic migrations | M | 1 |
+| 3 | ARCH-06: test suite (budget projection first) | L | 1 |
+| 4 | ARCH-15: N+1 fix in `compute_cc_by_category` | S | 2 |
+| 5 | ARCH-03: standardize validation/error handling/enums | S | 2 |
+| 6 | ARCH-09: FK delete rules via migration | S | 2 |
+| 7 | ARCH-04: agent tools delegate to services | M | 2 |
+| 8 | ARCH-16: shared response schemas (`response_model=` everywhere) | M | 3 |
+| 9 | ARCH-13/FE-10: query library on the frontend | M | 3 |
+| 10 | ARCH-11: expose deferred-transaction UI | S | 4 |
+| 11 | ARCH-18: CSV export | M | 4 |
+| 12 | ARCH-05/08/10/12/14/17/20: small hardening fixes | S each | 0–2 |
+
+**Overall assessment:** Production-ready for a single-user personal app, with the caveat that configuration defaults must fail fast before anything else. The architecture needs no redesign — invest in migrations, tests, and consistency.
diff --git a/codebase-review/02-backend.md b/codebase-review/02-backend.md
new file mode 100644
index 0000000..c22910a
--- /dev/null
+++ b/codebase-review/02-backend.md
@@ -0,0 +1,166 @@
+# Backend Review — WealthySmart
+
+> Hat: Senior Backend Engineer (Python / FastAPI / SQLModel). Focus: correctness, money & date math, query efficiency, PDF parsing, agent tools, migrations, tests.
+
+## Executive Summary
+
+The WealthySmart backend is a well-structured FastAPI application with a PostgreSQL database and SQLModel ORM. The codebase demonstrates solid architectural patterns (dependency injection, a real service layer, API tokenization) but contains several correctness and robustness gaps that deserve attention: timezone-naive `datetime.utcnow()` everywhere, `float` for all monetary values, a suspected off-by-one in billing-cycle math, N+1 query patterns in the agent tools, non-transactional multi-file PDF uploads, blocking subprocess calls inside `async` routes, and broad exception handlers that can mask failures. The codebase has no tests at all, which compounds every other risk — especially around the budget-cycle logic.
+
+## Strengths
+
+1. **Clean architecture** — well-organized service layer (`exchange_rate.py`, `budget_projection.py`), clear separation with one endpoint file per domain.
+2. **Robust exchange-rate handling** — multi-source fallback (BCCR → ExchangeRate-API → CoinGecko) with in-memory caching and last-known fallbacks.
+3. **Flexible budget system** — projection engine handles recurring items, 18th–18th billing cycles, and deferred transactions with carryover logic.
+4. **Security practices** — SHA-256 token hashing, OAuth2 bearer support, dual auth (JWT + API tokens), credentials hidden from logs.
+5. **API token management** — secure generation, expiration support, hash-based lookup.
+6. **Graceful degradation** — push-notification failures don't crash the request path; stale rates keep the app usable during API outages.
+7. **Comprehensive PDF services** — municipal-receipt and pension parsing with layered regex extraction.
+
+## Findings
+
+### [BE-01] `datetime.utcnow()` — Deprecated and Timezone-Naive — High
+**Location:** `auth.py:19`, `auth.py:51`, `budget.py:262`, `services/exchange_rate.py:139+`, 10+ call sites
+**Evidence:** `datetime.utcnow()` used throughout; deprecated since Python 3.12 in favor of `datetime.now(timezone.utc)`.
+**Why it matters:** Mixing naive and aware datetimes causes silent comparison bugs, and Postgres timestamp semantics depend on consistency. Costa Rica is UTC-6 year-round — naive "UTC" timestamps compared against local dates shift transactions across cycle boundaries near midnight.
+**Recommendation:** Mechanical replacement with `datetime.now(timezone.utc)`; audit each comparison site for naive/aware mixing; prefer `TIMESTAMPTZ` columns going forward.
+
+### [BE-02] Float Instead of Decimal for Money — High
+**Location:** `models/models.py:102, 104, 131, 184+` (all financial fields)
+**Evidence:** `amount: float`, `balance: float`, `buy_rate: float`, `saldo_final: float` across the schema.
+**Why it matters:** Binary floats can't represent most decimal amounts exactly; cumulative budget math, projections, and historical audits accumulate error. Industry standard for money is `Decimal`/`NUMERIC`.
+**Recommendation:** Migrate monetary fields to `Decimal` (`Field(max_digits=15, decimal_places=2)` → Postgres `NUMERIC`); update `budget_projection.py` and `exchange_rate.py` arithmetic. Do this *after* Alembic exists (one clean migration) and *with* tests in place.
+
+### [BE-03] Savings Accrual Lacks Atomicity — Medium
+**Location:** `services/savings_accrual.py:60`
+**Evidence:** `session.commit()` with no rollback path; if MEMP or MPAT accounts are missing, the function returns early with partial state possible.
+**Recommendation:** Raise early if required accounts are absent; wrap the mutation block so it commits entirely or not at all.
+
+### [BE-04] Pension Snapshot Dedup Done in Python — Medium
+**Location:** `agent/tools.py:269–278`
+**Evidence:** Loads all `PensionSnapshot` rows, then deduplicates by fund in a Python loop.
+**Recommendation:** `SELECT DISTINCT ON (fund) ... ORDER BY fund, period_end DESC` (PostgreSQL) or a window-function query.
+
+### [BE-05] N+1 in Municipal Receipts Water Readings — Medium
+**Location:** `agent/tools.py:320–344`
+**Evidence:** Loads receipts (line 324), then queries `WaterMeterReading` per receipt inside the loop (line 328) — 13 queries where 1–2 would do.
+**Recommendation:** Eager-load via `selectinload` or fetch all readings for the receipt IDs in one `IN (...)` query.
+
+### [BE-06] Possible Off-by-One in Billing-Cycle Calculation — High
+**Location:** `services/budget_projection.py:78–85, 103–108`
+**Evidence:** `get_cycle_range(year, month)` returns `(month/18, month+1/18)` while the comment says "cycle (M-1): from (M-1)/18 to M/18, paid with month M salary" — comment and code disagree about which cycle a month's budget uses.
+**Why it matters:** If wrong, every monthly budget is aligned to the wrong statement period — salary and expenses misreported by a month. If right, the comment is wrong and the next maintainer will "fix" working code.
+**Recommendation:** This is the **first thing to pin down with tests**: assert which cycle a Feb-15 transaction lands in for the March budget against known-good real data, then fix code or comment accordingly.
+
+### [BE-07] No Month/Year Validation in Date Helpers — Medium
+**Location:** `services/budget_projection.py:73–85`
+**Evidence:** `datetime(year, month, 18)` with no range check; month 13 raises an unhandled `ValueError` from deep inside projection math.
+**Recommendation:** Validate `1 <= month <= 12` and a sane year range at the helper boundary with a clear error message.
+
+### [BE-08] Duplicate Detection Is Incomplete — Medium
+**Location:** `endpoints/transactions.py:182–191`, `import_transactions.py:118–128`
+**Evidence:** Dedup keys off the nullable `reference` field; manual transactions and hash collisions/misses can create duplicates that skew analytics.
+**Recommendation:** Tighten the reference-hash inputs (include `card_last4`), and consider a unique constraint on `(date, merchant, amount, currency, source)` with an explicit override path for genuine same-day repeat purchases.
+
+### [BE-09] PDF Temp Files Not Cleaned on Timeout — Medium
+**Location:** `services/pension_pdf.py:46–50`, `services/municipal_receipt_pdf.py:79`
+**Evidence:** `subprocess.run(..., timeout=30/10)` is good, but on timeout/exception the `NamedTemporaryFile` cleanup path isn't guaranteed.
+**Recommendation:** Use `tempfile.TemporaryDirectory()` as a context manager so cleanup is unconditional.
+
+### [BE-10] Multi-File Uploads Are Not Transactional — High
+**Location:** `endpoints/pensions.py:104–162`, `municipal_receipts.py:197–228`
+**Evidence:** The loop commits per file; if file 2 fails after file 1 committed, the batch is half-applied with no undo.
+**Why it matters:** Partial uploads create inconsistent state the user can't easily see or revert.
+**Recommendation:** Either wrap the batch in one transaction (all-or-nothing) or keep per-file commits but return a precise per-file success/failure manifest (the response models already have `errors[]` — make the frontend show it, see UX-09).
+
+### [BE-11] Nullable FKs Without Delete Behavior — Medium
+**Location:** `models/models.py:144` (Transaction.category_id), `:258` (RecurringItem.category_id)
+**Evidence:** Optional FKs with no `ondelete` rule; deleting a category strands references.
+**Recommendation:** Same as ARCH-09 — explicit `SET NULL`/`CASCADE` per relationship, applied via migration.
+
+### [BE-12] Agent Session ContextVar Failure Is Cryptic — Medium
+**Location:** `agent/tools.py:51–52`
+**Evidence:** `_session_ctx.get()` raises bare `LookupError` if middleware didn't bind a session.
+**Recommendation:** Catch and re-raise as `RuntimeError("DB session not bound to agent context — tool called outside request scope")`.
+
+### [BE-13] Locale-Naive Amount Parsing in Paste Import — Low
+**Location:** `endpoints/import_transactions.py:77`
+**Evidence:** `match.group(1).replace(",", "")` assumes `,` is always a thousands separator.
+**Recommendation:** Document the assumed format (BAC statement format) in the docstring; add a sanity check (e.g., reject if the result is 100× off from a `.` interpretation).
+
+### [BE-14] Blocking Subprocess Inside Async Routes — Medium
+**Location:** `endpoints/pensions.py:105–162`, `municipal_receipts.py:197–228`
+**Evidence:** `async def upload_...()` calls `parse_pension_pdf()` which runs `subprocess.run()` synchronously — blocks the event loop for up to 30s per file.
+**Recommendation:** `await asyncio.to_thread(parse_pension_pdf, pdf_bytes)` (the codebase already uses `asyncio.to_thread` for rate refresh, so the pattern exists).
+
+### [BE-15] Hardcoded Default Credentials — High
+**Location:** `config.py:6, 8–9`
+**Evidence:** `SECRET_KEY: str = "change-me-in-production"`, `ADMIN_USERNAME: str = "admin"`, `ADMIN_PASSWORD: str = "admin"` (verified).
+**Why it matters:** A deploy that loses its env file boots wide open. See SEC-01 for the exploit framing.
+**Recommendation:** Remove defaults; raise at startup if unset or still default.
+
+### [BE-16] Agent Budget Tool Has No Year Bounds — Low
+**Location:** `agent/tools.py:212` (vs `endpoints/budget.py:131` which checks MIN_YEAR/MAX_YEAR)
+**Evidence:** REST endpoint validates year range; the agent tool calling the same projection logic does not — year 9999 reaches `compute_yearly_projection_with_cumulative()`.
+**Recommendation:** Apply the same MIN/MAX_YEAR bounds in the tool.
+
+### [BE-17] Agent Tools Have Fixed Limits, No Pagination — Low
+**Location:** `agent/tools.py:269, 327–344`
+**Evidence:** Hardcoded `limit=12` / `limit=50` with no offset parameter.
+**Recommendation:** Add optional `offset`/`limit` tool parameters so the assistant can answer questions about older history.
+
+### [BE-18] `except Exception: pass` Swallows Errors — Low
+**Location:** `services/exchange_rate.py:63–64, 93–94, 108–109, 122–123`
+**Evidence:** Bare pass on all exceptions in each rate-source fetcher.
+**Why it matters:** Network failures, schema changes in upstream APIs, and genuine bugs all fail identically and invisibly.
+**Recommendation:** Catch specific exceptions (`httpx.TimeoutException`, `json.JSONDecodeError`, `KeyError`) and `logger.warning(...)` per source so a dead source is observable.
+
+### [BE-19] Ad-hoc Migrations Without Atomicity — Medium
+**Location:** `db.py:13–77`
+**Evidence:** Sequential `ALTER TABLE` strings with per-statement try/except/rollback; a mid-sequence failure leaves a partially migrated schema with no version record.
+**Recommendation:** Alembic (duplicate of ARCH-02; listed here because the per-statement rollback pattern is itself a hazard).
+
+### [BE-20] Stale-Rate Cache Has No Timestamp — Low
+**Location:** `services/exchange_rate.py:31–37, 136–142`
+**Evidence:** `_last_known` caches carry no `last_updated`; consumers can't tell a fresh rate from a week-old one.
+**Recommendation:** Store a timestamp with the cached value and expose freshness (`{"rate": X, "as_of": ...}`) — the agent and UI can then warn on stale data (pairs with UX data-trust concerns).
+
+### [BE-21] Projection Runs ~15+ Queries Per Call — Medium
+**Location:** `services/budget_projection.py:114–222`
+**Evidence:** For each `TransactionSource`, 4–6 separate aggregate queries; 3 sources ≈ 15+ round-trips per month, ×12 for the yearly view.
+**Recommendation:** Collapse into one or two `GROUP BY source, transaction_type` aggregate queries.
+
+### [BE-22] PensionSnapshot Upsert Has a Race Window — Low
+**Location:** `models/models.py:329–335`
+**Evidence:** `UniqueConstraint("fund", "period_start", "period_end")` exists, but the upsert is an application-level check-then-insert; concurrent uploads of the same PDF can collide.
+**Recommendation:** Use `INSERT ... ON CONFLICT DO UPDATE` (`sqlalchemy.dialects.postgresql.insert`). Low priority — n8n is the only concurrent writer and runs daily.
+
+### [BE-23] Account Labels Not Unique — Low
+**Location:** `models/models.py:98–110`
+**Evidence:** `label: str` without a unique constraint; two accounts named "BAC" are allowed.
+**Recommendation:** Unique on `(bank, currency, label)` or at least surface duplicates in the UI.
+
+### [BE-24] Hardcoded Fallback Exchange Rates in Agent — Low
+**Location:** `agent/tools.py:77–99`
+**Evidence:** `sell = rate.sell_rate if rate else 600.0` and a magic `1.08` EUR multiplier in `get_net_worth`.
+**Why it matters:** The assistant reports a wrong net worth when the rate service is down — silently.
+**Recommendation:** Fall back to the latest DB-persisted rate; if none, say so in the tool output instead of inventing a number.
+
+### [BE-25] No Tests — Critical
+**Location:** `backend/` (no `tests/`, no pytest in `requirements.txt`)
+**Evidence:** Zero coverage on budget cycles, deferred logic, PDF parsing, auth.
+**Recommendation:** pytest + fixtures (in-memory SQLite or a test Postgres). Minimum first targets: cycle boundaries (resolves BE-06), deferred carryover, PDF parser golden files, token auth paths.
+
+## Quick Wins (under 1 hour each)
+
+1. Startup check rejecting default `SECRET_KEY`/admin creds (`config.py`) — BE-15
+2. `s/datetime.utcnow()/datetime.now(timezone.utc)/` sweep — BE-01 (verify comparisons after)
+3. Month/year bounds in `budget_projection.py` helpers — BE-07
+4. `DISTINCT ON` for pension snapshots — BE-04
+5. Eager-load water readings — BE-05
+6. `TemporaryDirectory()` in both PDF parsers — BE-09
+7. Year bounds in agent budget tool — BE-16
+8. Specific exceptions + logging in `exchange_rate.py` — BE-18
+9. `asyncio.to_thread` around PDF parsing — BE-14
+10. Clear error message for unbound agent session — BE-12
+
+**Recommended sequence:** tests for cycle math first (BE-25 → BE-06), then the Decimal migration (BE-02) once Alembic (BE-19/ARCH-02) exists to carry it.
diff --git a/codebase-review/03-frontend.md b/codebase-review/03-frontend.md
new file mode 100644
index 0000000..98e50d2
--- /dev/null
+++ b/codebase-review/03-frontend.md
@@ -0,0 +1,158 @@
+# Frontend Review — WealthySmart
+
+> Hat: Senior Frontend Engineer (React 19 / TypeScript / Vite / Tailwind 4 / Hono / CopilotKit). Focus: data fetching, state, type safety, auth flow, accessibility, the Hono gateway.
+
+## Executive Summary
+
+WealthySmart's frontend is a well-structured React 19 + Vite + TypeScript app behind a Hono gateway server. The codebase shows good practices in component organization, Tailwind styling, and accessibility basics. The dominant weakness is the hand-rolled data-fetching layer: no request cancellation, no caching/deduplication, and inconsistent error/loading states — several pages swallow API errors into `console.error` and render empty UI. The CopilotKit integration is sophisticated but leans on heavy middleware workarounds (message dedup, snapshot reconciliation) that indicate upstream fragility and currently swallow their own errors. Auth uses httpOnly cookies (`ws_token`) over same-origin requests — a solid choice — but the 401 handling can redirect-loop and the auth probe treats network failure as logged-out.
+
+## Strengths
+
+- **Type safety** — comprehensive interfaces in `api.ts` for Transaction, RecurringItem, Budget types; only a couple of `any` escapes.
+- **Component architecture** — clean decomposition into pages, hooks (`useBudget`), and contexts (Theme, Privacy, Auth).
+- **Accessibility fundamentals** — focus rings, aria-labels, title attributes on interactive elements; privacy mode via `data-sensitive` masking; logical heading hierarchy.
+- **Error messages where they exist** — `TransactionModal` handles 409 conflicts with a user-readable message.
+- **Format helpers** — locale-aware CRC formatting (`formatAmount`, `formatCRC`) used consistently in most modules.
+- **Hono gateway** — cleanly proxies FastAPI and hosts the CopilotKit runtime; `DeduplicateToolCallMiddleware` / `ReconcileSnapshotMiddleware` address known CopilotKit issues head-on.
+- **Responsive baseline** — mobile menu, adaptive layouts; tables are the weak spot (see FE/UX findings).
+
+## Findings
+
+### [FE-01] No Request Cancellation in `useBudget` — High
+**Location:** `src/hooks/useBudget.ts:52–59`
+**Evidence:**
+```typescript
+useEffect(() => {
+ fetchProjection();
+ fetchRecurringItems();
+}, [fetchProjection, fetchRecurringItems]);
+```
+**Why it matters:** Changing `year`/`selectedMonth` while a request is in flight lets the stale response overwrite newer state. No AbortController or request-ID tracking anywhere in the app.
+**Recommendation:** Abort on dependency change (`return () => ctrl.abort()`), or adopt TanStack Query which solves this class wholesale (see FE-10).
+
+### [FE-02] Analytics Swallows Errors, No Loading State — Medium
+**Location:** `src/pages/Analytics.tsx:77–95`
+**Evidence:** `Promise.all([...]).then(...).catch(console.error)` — failures leave silently empty charts; no skeleton during fetch.
+**Recommendation:** `loading` / `error` / retry states with visible UI.
+
+### [FE-03] Transaction Fetch Races in Budget Page — High
+**Location:** `src/pages/Budget.tsx:44–77`
+**Evidence:** `fetchTransactions` re-fires on `txSearch`/`txSource`/`selectedMonth` changes with no cancellation; last-to-resolve wins even if stale.
+**Recommendation:** AbortController per request keyed to the effect; or query library.
+
+### [FE-04] Unguarded Array Indexing in Pensions ROI Fallback — Medium
+**Location:** `src/pages/Pensions.tsx:292–315`
+**Evidence:** Fallback computes `chartData[len - 1][key] - chartData[len - 2][key] - ...`; with `len < 2` this silently yields 0/negative garbage.
+**Recommendation:** Guard `if (len < 2) { acc[key] = 0; }` before indexing.
+
+### [FE-05] Post-Upload Refetch Can Hang the Upload UI — Medium
+**Location:** `src/pages/Pensions.tsx:343–365`
+**Evidence:** `await loadData()` after upload has no timeout; "Procesando…" persists indefinitely if the backend stalls.
+**Recommendation:** Timeout the refetch and surface an actionable message.
+
+### [FE-06] `catch (err: any)` in TransactionModal — Medium
+**Location:** `src/components/TransactionModal.tsx:97`
+**Evidence:** `err.response?.status === 409` assumes a shape; non-HTTP errors (network) silently fall through to `console.error` with no user message.
+**Recommendation:** Type-guard with the app's `ApiError`; always set a user-visible error in the else branch.
+
+### [FE-07] localStorage Read in Effect Instead of Lazy Initializer — Low
+**Location:** `src/contexts/theme-context.tsx:16–24`, `src/contexts/privacy-context.tsx:13–15`
+**Evidence:** Initial theme read happens in `useEffect`, causing a flash of wrong theme on mount.
+**Recommendation:** `useState(() => localStorage.getItem("theme") ?? ...)` lazy initializer.
+
+### [FE-08] No Security Headers from the Hono Gateway — Low
+**Location:** `frontend/server.ts:379–416`
+**Evidence:** No CSP, `X-Frame-Options`, `X-Content-Type-Options`, or `Referrer-Policy` on served responses.
+**Recommendation:** One small Hono middleware (pairs with backend SEC-07; the user-facing HTML is served from here, so this is the layer that matters most for CSP/frame protection).
+
+### [FE-09] Coupled Effects in `useBudget` — Medium
+**Location:** `src/hooks/useBudget.ts:27–35`
+**Evidence:** Projection and recurring-items fetches share one effect; failure modes and re-run triggers are entangled.
+**Recommendation:** One effect per concern with explicit dependencies.
+
+### [FE-10] No Query Caching or Deduplication — High
+**Location:** `src/lib/api.ts:15–66` and all consumers
+**Evidence:** Every `api.get()` is a fresh fetch; parallel consumers of the same endpoint each hit the network; every page re-fetches everything on mount.
+**Why it matters:** This is the root cause behind FE-01/02/03/09/24 and ARCH-13. One architectural decision fixes the whole class.
+**Recommendation:** Adopt **TanStack Query**: per-key caching, dedup, cancellation, retries, `isLoading`/`isError` states, and mutation invalidation. Migrate page by page (Budget first).
+
+### [FE-11] CopilotKit Middleware Swallows Parse Errors — Medium
+**Location:** `frontend/server.ts:350–368`
+**Evidence:** `catch { return; }` in `beforeRequestMiddleware` — malformed bodies silently skip orphan-tool-call pairing, producing confusing downstream runtime failures.
+**Recommendation:** `console.error` with context before returning; given the documented history of card-persistence bugs (see `reference_agui_client` memory), observability here pays for itself.
+
+### [FE-12] Month Detail Rendered Without Existence Check — Medium
+**Location:** `src/pages/Budget.tsx:115–154`
+**Evidence:** Month navigation renders detail UI without checking `monthDetail` exists for the selection.
+**Recommendation:** `monthDetail ? : loading ? : `.
+
+### [FE-13] XSS Posture Is Fine — Watch `dangerouslySetInnerHTML` — Informational
+**Location:** `src/components/TransactionList.tsx:129`, `src/components/chat/ChatCards.tsx:128`
+**Evidence:** Merchant/category strings render through JSX (auto-escaped). No `dangerouslySetInnerHTML` found. Not a current bug — recorded as a guardrail since this data originates from emails (attacker-influenceable).
+
+### [FE-14] No Runtime Validation of API Responses — Medium
+**Location:** `src/lib/api.ts` and all consumers
+**Evidence:** Response types are compile-time assertions only; backend schema drift produces `undefined` rendering or crashes far from the cause.
+**Recommendation:** Either Zod-parse at the API-client boundary for the few critical shapes, or (better long-term) generate types from FastAPI's OpenAPI schema once ARCH-16 lands (`openapi-typescript`).
+
+### [FE-15] Auth Probe Treats Network Failure as Logged Out — Medium
+**Location:** `src/AuthContext.tsx:22–29`
+**Evidence:** `.catch(() => setAuthenticated(false))` — a transient network blip on app load kicks an authenticated user to the login screen.
+**Recommendation:** Distinguish network errors (retry once, or keep an "unknown" state) from a real 401.
+
+### [FE-16] Missing `aria-pressed` on Toggle Buttons — Low
+**Location:** `src/pages/Pensions.tsx:515–530` (fund visibility toggles)
+**Evidence:** Toggles convey state visually only.
+**Recommendation:** `aria-pressed={visibleFunds.has(key)}`; audit other toggles (privacy mode, theme).
+
+### [FE-17] `en-US` Dates in an es-CR App — Low
+**Location:** `src/pages/Analytics.tsx:238–240` (chart `labelFormatter`), also `src/lib/format.ts:19–21`
+**Evidence:** Chart tooltips show "Jan", "Feb" while the page is in Spanish.
+**Recommendation:** Centralize on `es-CR` (see UX-19 for the full localization sweep).
+
+### [FE-18] `useBudget` Return Object Not Memoized — Low
+**Location:** `src/hooks/useBudget.ts:86–103`
+**Evidence:** Fresh object identity every render; consumers using it as a dependency re-run unnecessarily.
+**Recommendation:** Wrap in `useMemo` (moot if migrated to TanStack Query).
+
+### [FE-19] Unbounded Upload-Result List — Low
+**Location:** `src/pages/Pensions.tsx:822–865`
+**Evidence:** All returned snapshots render without scroll containment or truncation.
+**Recommendation:** Slice to ~10 with "and N more".
+
+### [FE-20] Dead Code: `AgentHomeClient.tsx` — Low
+**Location:** `src/components/AgentHomeClient.tsx` vs `src/pages/Asistente.tsx`
+**Evidence:** Two near-identical components; only `Asistente.tsx` is routed.
+**Recommendation:** Delete `AgentHomeClient.tsx`.
+
+### [FE-21] (Withdrawn) — the shadcn re-export concern was speculative; no concrete evidence found. Dropped.
+
+### [FE-22] 401 Handler Can Redirect-Loop — Medium
+**Location:** `src/lib/api.ts:48–52`
+**Evidence:** On 401: call logout, `window.location.replace("/login")`. If any call from the login page itself 401s, this loops.
+**Recommendation:** Skip the redirect when already on `/login`.
+
+### [FE-23] Proxy Has No Error Handling for Backend Failures — Medium
+**Location:** `frontend/server.ts:385–402`
+**Evidence:** `proxyToBackend` returns `fetch(target, init)` with no try/catch; a connection refusal becomes an opaque 500.
+**Recommendation:** try/catch → log → `c.json({ error: "Backend unavailable" }, 502)`.
+
+### [FE-24] No Timeouts on Any Fetch — Medium
+**Location:** All pages
+**Evidence:** No `AbortSignal.timeout()` anywhere; hung backends mean indefinite spinners.
+**Recommendation:** Default `AbortSignal.timeout(30_000)` in the shared `request()` helper in `api.ts` — one change covers the app.
+
+## Quick Wins (under 1 hour each)
+
+1. `es-CR` locale in Analytics chart labels — FE-17
+2. Delete `AgentHomeClient.tsx` — FE-20
+3. Security-headers middleware in `server.ts` — FE-08
+4. Guard `/login` in the 401 redirect — FE-22
+5. try/catch + 502 in `proxyToBackend` — FE-23
+6. `AbortSignal.timeout(30000)` in the shared request helper — FE-24
+7. Guard `chartData.length >= 2` in Pensions ROI — FE-04
+8. `aria-pressed` on fund toggles — FE-16
+9. Log CopilotKit middleware parse failures — FE-11
+10. Lazy initializers for theme/privacy contexts — FE-07
+
+**Strategic recommendation:** Most Medium/High findings here share one root cause — the absence of a query layer. Adopting TanStack Query (Plan Phase 3) retires FE-01, FE-03, FE-09, FE-10, FE-18, FE-24 and ARCH-13 in one move, and makes FE-02-style error states the default rather than per-page work.
diff --git a/codebase-review/04-security.md b/codebase-review/04-security.md
new file mode 100644
index 0000000..255937f
--- /dev/null
+++ b/codebase-review/04-security.md
@@ -0,0 +1,132 @@
+# Security Review — WealthySmart
+
+> Hat: Senior Application Security Engineer. Defensive review of the owner's own app (wealth.cescalante.dev) for hardening. All findings verified against actual code; key claims re-checked by the synthesizer (see README corrections).
+
+## Executive Summary
+
+The most important finding is configuration, not code: `backend/app/config.py` ships **working fallback credentials** (`admin`/`admin`, a known `SECRET_KEY`) that take effect silently if a deploy ever runs without its env file — and the deploy pipeline regenerates `.env.prod` on every deploy, making that scenario plausible. Beyond that: login endpoints use non-constant-time `!=` comparison with **no rate limiting**, CORS is `allow_origins=["*"]` with credentials enabled, JWTs live for 30 days, and PDF upload endpoints accept files of any size and type. On the positive side, the app uses parameterized queries throughout, safe subprocess invocation (no `shell=True`, controlled args, timeouts), hashed API tokens with expiry checks, and httpOnly + SameSite=Lax cookies. All critical fixes are low-effort.
+
+**Note (good news, verified):** `.env` and `.env.prod` are properly gitignored and have never been committed — no secret exposure via the repository.
+
+## Threat Model
+
+**Assets:** personal financial data (accounts, transactions, balances, pensions, water usage), admin credentials, JWT signing key, n8n API tokens, OpenAI API key, VAPID keys.
+
+**Actors:**
+- Internet attacker reaching wealth.cescalante.dev (it is publicly exposed)
+- Malicious website visited by the logged-in user (CORS/CSRF surface)
+- Credential brute-forcer (no rate limit, fixed username)
+- Holder of a stolen token (30-day validity window)
+- **Indirect prompt injector** — transaction descriptions originate from emails parsed by n8n; an attacker who controls a merchant name or email content gets text into the LLM agent's context
+
+**Entry points:** `/api/auth/login` (×2 implementations), `/api/v1/pensions/upload`, `/api/v1/municipal-receipts/upload`, `/api/copilotkit/*` (Hono), `/api/v1/agent/agui`, n8n token-authenticated POSTs.
+
+## Findings
+
+### SEC-01: Working Fallback Credentials in Default Configuration — Critical
+**Location:** `backend/app/config.py:6, 9, 10` (verified)
+**Evidence:**
+```python
+class Settings(BaseSettings):
+ DATABASE_URL: str = "postgresql://wealthy_user:wealthy_pass@db:5432/wealthysmart"
+ SECRET_KEY: str = "change-me-in-production"
+ ADMIN_USERNAME: str = "admin"
+ ADMIN_PASSWORD: str = "admin"
+```
+**Exploit scenario:** Deploy pipeline regenerates `.env.prod` from Gitea secrets each deploy (see `reference_gitea_deploy`). A pipeline regression or missing secret silently boots the app accepting `admin:admin`, and the known `SECRET_KEY` lets anyone forge valid JWTs even *without* logging in.
+**Remediation:** Remove defaults; validate at startup:
+```python
+SECRET_KEY: str # no default
+@model_validator(mode="after")
+def _no_weak_defaults(self):
+ if self.SECRET_KEY in ("", "change-me-in-production"):
+ raise ValueError("SECRET_KEY must be set")
+ if self.ADMIN_PASSWORD in ("", "admin"):
+ raise ValueError("ADMIN_PASSWORD must be set")
+ return self
+```
+A failed boot is loud (healthcheck fails, deploy aborts) — exactly what you want.
+
+### SEC-02: Non-Constant-Time Credential Comparison — High
+**Location:** `backend/app/main.py:165–169`, `backend/app/api/v1/endpoints/auth.py:12–19`
+**Evidence:** Both login implementations:
+```python
+if (body.username != settings.ADMIN_USERNAME
+ or body.password != settings.ADMIN_PASSWORD):
+ raise HTTPException(status_code=401, ...)
+```
+**Exploit scenario:** Short-circuit `or` plus string `!=` leaks timing information distinguishing username-fail from password-fail and partially leaking prefix matches under many measurements. Marginal for a remote attacker, but the fix is one line and the password is also stored in plaintext config.
+**Remediation:** `hmac.compare_digest()` on both fields, both endpoints. Better: store a bcrypt/argon2 hash of the admin password (`passlib`) instead of the plaintext in env.
+
+### SEC-03: No Rate Limiting on Login — High
+**Location:** Both login endpoints (`main.py:163–179`, `endpoints/auth.py:10–21`)
+**Evidence:** No limiter, lockout, or backoff anywhere in the codebase.
+**Exploit scenario:** Username is effectively fixed ("admin"); an attacker brute-forces the password at full line speed against a public endpoint.
+**Remediation:** `slowapi` with `5/minute` per IP on both login routes, or a simple in-memory failure counter with exponential backoff (single-process app, in-memory is fine). Also consider consolidating to **one** login endpoint — two implementations means two places to harden forever.
+
+### SEC-04: CORS `["*"]` with Credentials Enabled — Medium
+**Location:** `backend/app/main.py:79–85` (verified line 81)
+**Evidence:**
+```python
+app.add_middleware(CORSMiddleware,
+ allow_origins=["*"], allow_credentials=True,
+ allow_methods=["*"], allow_headers=["*"])
+```
+**Exploit scenario:** Starlette reflects the request Origin when `allow_credentials=True`, so any website gets credentialed CORS approval. Mitigating factor: the `ws_token` cookie is `SameSite=Lax`, so browsers won't attach it to cross-site XHR/fetch — which currently blunts this from Critical to Medium. But the protection is one cookie-attribute change away from vanishing, and `Authorization`-header flows aren't SameSite-protected.
+**Remediation:** `allow_origins=["https://wealth.cescalante.dev", "http://localhost:3000"]`, explicit methods and headers.
+
+### SEC-05: 30-Day JWT Expiry — Medium
+**Location:** `backend/app/config.py:7` (verified), `backend/app/auth.py:18–20`
+**Evidence:** `ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 30 # 30 days`
+**Exploit scenario:** A stolen cookie/token works for a month; there is no revocation mechanism for JWTs (only API tokens are revocable).
+**Remediation:** For a single-user personal app, 24h–7d is a reasonable convenience/security balance (8h would force daily logins). Pick e.g. 7 days, or implement short access + refresh tokens if login friction matters.
+
+### SEC-06: No Upload Size/Type Validation — Medium
+**Location:** `backend/app/api/v1/endpoints/pensions.py:104–162`, `municipal_receipts.py:197–228`
+**Evidence:** `files: list[UploadFile]` then `await file.read()` — unbounded read, no content-type or magic-byte check (endpoints *are* authenticated).
+**Exploit scenario:** Memory-exhaustion DoS via huge upload (requires auth — the n8n token or the user's own session — so primarily a robustness issue); junk files reach `pdftotext`.
+**Remediation:** 10 MB cap via bounded read, require `pdf_bytes.startswith(b"%PDF")`, record per-file rejections in the existing `errors[]` response field.
+
+### SEC-07: Missing Security Headers — Low
+**Location:** `backend/app/main.py` (absent middleware); more importantly `frontend/server.ts` since it serves the HTML
+**Evidence:** No CSP, `X-Content-Type-Options`, `X-Frame-Options`, or HSTS set by either layer.
+**Remediation:** Set headers at the Hono layer (the user-facing one): `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, and a CSP once inline-script usage is audited. HSTS can be set at nginx-proxy.
+
+### SEC-08: Cookie `secure=False` Relies on a Comment — Low
+**Location:** `backend/app/main.py:177`
+**Evidence:** `secure=False, # set True behind TLS in production via nginx` — but nothing actually sets it to True in production; nginx terminating TLS does not add the Secure attribute to an app-issued cookie.
+**Exploit scenario:** The cookie would be sent over plain HTTP if the user ever hits an http:// URL for the domain (pre-HSTS first visit, or a downgrade).
+**Remediation:** Drive it from config: `secure=settings.COOKIE_SECURE` (True in prod env). Pair with HSTS.
+
+### SEC-09: LLM Agent — Indirect Prompt Injection Surface — Medium (added by synthesis)
+**Location:** `backend/app/agent/tools.py` (tool outputs include merchant names, notes), data path: email → n8n → `POST /transactions/` → agent context
+**Evidence:** Transaction descriptions originate from external emails. When the assistant lists recent transactions, attacker-influenceable text (a crafted merchant name) enters the model's context. Mitigating factors: all agent tools are **read-only queries** (verified — no tool mutates data), and the agent endpoint requires authentication, so impact is bounded to misleading the user, not data modification or exfiltration to third parties.
+**Remediation:** Keep tools read-only as a hard rule (document it); if write-tools are ever added, require explicit user confirmation in the UI (`renderAndWaitForResponse`) for every mutation. Consider delimiting untrusted strings in tool outputs.
+
+### SEC-10: Positive Findings (No Issue) — Informational
+- **API token handling is correct** (`auth.py:42–54`): SHA-256 hash lookup, `is_active` flag, expiry checked against now. Tokens are revocable.
+- **Subprocess usage is safe** (`pension_pdf.py:46–53`, `municipal_receipt_pdf.py:79–86`): list-args, no `shell=True`, timeouts, Python-generated temp filenames — no command-injection path.
+- **No raw SQL string interpolation found** — ORM/parameterized throughout (the ad-hoc migrations in `db.py` are static strings).
+- **httpOnly + SameSite=Lax cookie** — correct baseline against XSS token theft and CSRF.
+- **`.env` files properly gitignored, never committed** (verified via `git ls-files` and history).
+
+## Hardening Checklist
+
+**Priority 1 — do before anything else (≈half a day total):**
+- [ ] SEC-01: remove config defaults; fail-fast startup validation
+- [ ] SEC-02: `hmac.compare_digest` (or hashed admin password) in both login endpoints
+- [ ] SEC-03: rate-limit login (5/min/IP); consolidate the duplicate login endpoints
+- [ ] SEC-04: pin CORS origins
+
+**Priority 2 — same week:**
+- [ ] SEC-05: reduce token expiry (7 days suggested)
+- [ ] SEC-06: upload size cap + `%PDF` magic check
+- [ ] SEC-07: security headers at the Hono layer; HSTS at nginx
+- [ ] SEC-08: `secure=True` cookie in production via config
+
+**Priority 3 — ongoing posture:**
+- [ ] Keep agent tools read-only by policy; confirm-before-write if that ever changes (SEC-09)
+- [ ] Audit logging for login attempts and token creation
+- [ ] Scope n8n API tokens to the endpoints they need (currently any valid token hits any endpoint)
+- [ ] Database backup with encryption; test restore
+- [ ] Dependency update cadence (Dependabot/Renovate on `requirements.txt` and `package.json`)
diff --git a/codebase-review/05-poweruser.md b/codebase-review/05-poweruser.md
new file mode 100644
index 0000000..cd67a81
--- /dev/null
+++ b/codebase-review/05-poweruser.md
@@ -0,0 +1,161 @@
+# Poweruser Review — WealthySmart
+
+> Hat: Daily power user of the app + senior product engineer. Focus: real user journeys, feedback loops, data trust, consistency, missing features.
+
+## Executive Summary
+
+WealthySmart has solid fundamentals: clean routing, consistent UI patterns, bilingual support, and thoughtful features like pension projections and municipal receipt tracking. But daily use exposes friction: **no success/error feedback after mutations** (did my delete work?), no error recovery anywhere (failed loads look like empty data), three different month-navigation patterns across modules, a search box that silently does nothing for non-credit-card sources, and — most corrosive to trust — **silent n8n parse failures** that leave invisible gaps in transaction history with no reconciliation view. The data tables are dense and not mobile-adapted. The Asistente is pleasant but read-only and ephemeral. None of these are deep architectural problems; they are a backlog of S/M-effort polish items plus two larger trust-building features (sync status, import review).
+
+## What Works Well
+
+- **Navigation & layout** — well-organized sidebar (General, Finanzas, Servicios); smooth mobile menu.
+- **Strong typed data model** on the client — comprehensive interfaces across all modules.
+- **Privacy mode** — `data-sensitive` blur with an accessible header toggle. Great for screen-sharing.
+- **Thoughtful finance features** — pension projections with adjustable assumptions, recurring items with frequency + override logic, billing-cycle awareness, deferred transactions.
+- **Accessibility baseline** — focus-visible rings, aria-labels, disabled-state management.
+- **Charts** — Recharts configured properly with tooltips/legends across Analytics, Proyecciones, Pensions, ServiciosMunicipales.
+- **Import result summaries** — paste-import and PDF upload report imported/updated/duplicate counts.
+
+## Findings
+
+### UX-01 | No success feedback after mutations — High
+**Location:** `Budget.tsx:180–183`, `RecurringItemsManager.tsx:57–69`, `TransactionList.tsx:64–74`, Pensions upload handlers
+**Evidence:** Mutations call `onRefresh()`/`refresh()` with zero confirmation — no toast, no inline message. `TransactionList.tsx:68` deletes then refreshes silently.
+**User impact:** "Did the delete go through?" Users infer success from list churn — anxiety for irreversible actions.
+**Recommendation:** Add a toast layer (Sonner) and emit success/error on every mutation. Effort: **M** (library + sweep).
+
+### UX-02 | No error recovery flow anywhere — High
+**Location:** `Analytics.tsx:84–94`, `ServiciosMunicipales.tsx:198–211`, `PasteImportModal.tsx:42–46`, `Budget.tsx:44–76`
+**Evidence:** `.catch(console.error)` or silent catches; Budget's `fetchTransactions` has no error handling at all.
+**User impact:** API failure is indistinguishable from "no data." No retry affordance.
+**Recommendation:** Standard `loading → error(retry) → data/empty` states on every page. Effort: **M** (trivial if TanStack Query lands first).
+
+### UX-03 | Three different month/date-navigation patterns — Medium
+**Location:** `Budget.tsx` + `Proyecciones.tsx` (chevrons), `Analytics.tsx` (`BillingCycleSelector` dropdown), `Pensions.tsx` (fixed last-12-months, no navigation)
+**User impact:** Inconsistent mental model; Pensions history before 12 months is unreachable.
+**Recommendation:** One shared period-navigator component; add history browsing to Pensions. Effort: **M**.
+
+### UX-04 | Search silently inert for non-credit-card sources — Medium
+**Location:** `Budget.tsx:42–43, 164–188`
+**Evidence:** `txSearch` only feeds CREDIT_CARD queries; on the "Efectivo y Transferencias" tab the visible search box does nothing.
+**User impact:** Search appears broken.
+**Recommendation:** Make search work for all sources, or hide the field when inapplicable. Effort: **S**.
+
+### UX-05 | Deferred-transaction toggle gives no feedback — Medium
+**Location:** `transaction-columns.tsx:68–72`, `TransactionList.tsx:37`
+**Evidence:** Toggle PATCHes then refetches; no toast, no row styling change beyond a small "Diferida" badge.
+**User impact:** User can't tell the deferral took effect without hunting.
+**Recommendation:** Toast on toggle + muted/strikethrough styling on deferred rows. Effort: **S**. (Pairs with ARCH-11: add the field to `TransactionModal` too.)
+
+### UX-06 | Dirty-form dismissal loses data silently — Low
+**Location:** `RecurringItemDialog.tsx:50–100`
+**Recommendation:** ConfirmDialog on close when dirty. Effort: **S**.
+
+### UX-07 | "All time" English label in Spanish UI — Low
+**Location:** `BillingCycleSelector.tsx:52`
+**Recommendation:** "Todo el período". Effort: **S** (part of the UX-19 locale sweep).
+
+### UX-08 | Login page is bare — Low
+**Location:** `Login.tsx`
+**Evidence:** No remember-me, no error differentiation. (Password reset is N/A for a single-user env-configured credential.)
+**Recommendation:** Low priority; longer cookie life (bounded by SEC-05) already covers persistence. Effort: **S**.
+
+### UX-09 | Upload errors returned by the API are never displayed — Medium
+**Location:** Pensions upload result rendering
+**Evidence:** Backend returns `errors: string[]`; UI shows counts only — a failed parse looks like "Import complete, 0 imported."
+**User impact:** User can't tell *why* an upload failed (wrong file? corrupt PDF?).
+**Recommendation:** Render `result.errors` in an Alert list. Effort: **S**.
+
+### UX-10 | No undo for destructive operations — Medium
+**Location:** `TransactionList.tsx:64–74`, RecurringItemsManager delete
+**Evidence:** ConfirmDialog exists, but post-confirm deletion is immediate and irreversible.
+**Recommendation:** Toast-with-Undo grace period (delay the API call ~5s), or DB soft-delete + restore endpoint. Effort: **M**.
+
+### UX-11 | Transaction table is poor on mobile — Medium
+**Location:** `DataTable.tsx:57–80`, `transaction-columns.tsx`
+**Evidence:** 5+ columns force horizontal scroll at <640px; tiny text.
+**Recommendation:** Card-stack rendering on small screens (merchant/amount primary; category/bank secondary) or column-visibility tiers. Effort: **M**.
+
+### UX-12 | Pension projections use hardcoded assumptions — Medium
+**Location:** `Pensions.tsx:76–116`
+**Evidence:** `FUNDS_DEFAULT` hardcodes `startBalance`, `monthlyContribution`, `annualRate`; `applySnapshots()` updates only `saldo_final`.
+**User impact:** Retirement projections drift from reality as contributions change.
+**Recommendation:** Editable per-fund assumptions persisted server-side (settings or a `PensionAssumption` table). Effort: **M**.
+
+### UX-13 | Cycle filter only applies to one of three Analytics charts — Medium
+**Location:** `Analytics.tsx:71–95`
+**Evidence:** Category breakdown respects the selected cycle; monthly-trend and daily-spending queries ignore it.
+**User impact:** User picks a cycle and two charts silently show all-time data.
+**Recommendation:** Pass cycle params to all three endpoints (backend support needed) or visually scope the selector to the one chart it affects. Effort: **M**.
+
+### UX-14 | Empty states are dead ends — Low
+**Location:** `Budget.tsx:186`, `Salarios.tsx:157`, `TransactionList.tsx:81`
+**Recommendation:** `emptyAction` prop rendering an "Add first item" button. Effort: **S**.
+
+### UX-15 | Destructive-action confirmation applied inconsistently — Medium
+**Location:** `YearlyOverview.tsx:70–72` (clearing a balance override has no confirm; transaction/recurring deletes do)
+**Recommendation:** ConfirmDialog on every destructive action. Effort: **S**.
+
+### UX-16 | Asistente is read-only with no export — Low
+**Location:** `Asistente.tsx:56–79`
+**Evidence:** Only `SpendingSummaryCard` renders; no CSV export, no report download, no chat persistence.
+**Recommendation:** "Export CSV" on cards; longer-term, persist useful summaries. Effort: **M**.
+
+### UX-17 | n8n parser failures are invisible — High
+**Location:** Systemic (n8n → API ingestion path); no sync-status surface anywhere in the app
+**Evidence:** If BAC changes its email format, the n8n flow fails in n8n's own logs; the app just shows a gap in transactions.
+**User impact:** The core promise — "your finances, automatically tracked" — fails silently. The user discovers missing weeks at month-end close.
+**Recommendation:** A Sync Status page: last-received timestamp per source (BAC, salary, municipal, pension), expected-cadence warnings ("no BAC email in 7 days"), n8n error visibility. Backend can derive most of this from existing data (`max(created_at) per source`) — no n8n integration needed for v1. Effort: **M** for derived v1, **L** for full n8n integration.
+
+### UX-18 | No preview when editing recurring items — Medium
+**Location:** `Budget.tsx:115–201`, `useBudget.ts`
+**Evidence:** Changing frequency MONTHLY→QUARTERLY shows impact only after save.
+**Recommendation:** Before/after monthly-total preview in the dialog. Effort: **M**.
+
+### UX-19 | Mixed-language dates and labels across the app — Low (but pervasive)
+**Location:** `format.ts:19–21` ('en-US'), `Budget.tsx:12–15` (Spanish `MONTH_NAMES`), `Salarios.tsx:42–49` ('en-US'), `ServiciosMunicipales.tsx:54–57` (`MONTH_NAMES_ES`)
+**Evidence:** Three separate month-name implementations in two languages; `formatDate` returns English.
+**Recommendation:** One locale-aware date/format module (`Intl` with `es-CR`), delete the per-page constants. Effort: **M** (sweep).
+
+### UX-20 | Duplicates are counted but never reviewable — High
+**Location:** Import flows (paste-import, PDF upload) — UI shows `duplicates: 3` with no detail
+**Evidence:** No way to see *which* rows were skipped as duplicates or to override a false-positive dedup.
+**User impact:** Data-integrity trust gap; a false-positive dedup silently loses a real transaction.
+**Recommendation:** Import-review response listing skipped rows (merchant/date/amount + matched existing row), with a "import anyway" override. Effort: **L** (backend detail + UI).
+
+### UX-21 | Balance override input has no sanity check — Low
+**Location:** `YearlyOverview.tsx:55–72`
+**Recommendation:** Show "Calculated: ₡X" beside the input; warn on large deviation. Effort: **S**.
+
+### UX-22 | Proyecciones → Budget navigation is one-way — Low
+**Location:** `Proyecciones.tsx:96–99`
+**Recommendation:** Back affordance/breadcrumb when arriving from Proyecciones. Effort: **S**.
+
+### UX-23 | Chat cards are ephemeral — Low
+**Location:** `Asistente.tsx`, `ChatCards.tsx`
+**Evidence:** Refresh loses everything; no save/share affordance on cards.
+**Recommendation:** Export/copy button on `SpendingSummaryCard`. Effort: **S**.
+
+### UX-24 | Water-meter chart uses raw meter IDs — Low
+**Location:** `ServiciosMunicipales.tsx:48–52`
+**Evidence:** Legend shows '7335'/'7345'/'9345'; hardcoded colors; unknown meters fall back to one default color.
+**Recommendation:** User-editable meter labels ("Casa", "Cochera") in settings. Effort: **S**.
+
+### UX-25 | Sticky header/sidebar scroll jank — Low
+**Location:** `Layout.tsx:110, 153`
+**Recommendation:** Consistent scroll container (`overflow-y-auto` on main). Effort: **S**.
+
+## Feature Wishlist (prioritized)
+
+1. **Transaction search & filtering everywhere** — full-text on merchant/notes/reference; date-range, amount-range, category filters. *High impact, M.*
+2. **Bulk transaction actions** — multi-select → bulk re-categorize / defer / delete-with-undo. Statement import currently means 50 one-by-one edits. *High impact, M.*
+3. **Sync status & reconciliation view** — last import per source, cadence warnings, failed-parse visibility, duplicate review (UX-17 + UX-20 together). *High impact, L. This is the single biggest trust feature.*
+4. **CSV export** — transactions, salarios, pensions; for taxes/accountant. *Medium impact, M.*
+5. **Budget override history & revert** — when/why an override was set. *Medium impact, S.*
+6. **What-if scenario planner** — "save ₡50k more/month" against the 5-year projection. *Medium impact, M.*
+7. **Exchange-rate history & per-date override** for reconciling old statements. *Medium impact, S.*
+8. **PWA polish** — installable, offline-readable balances. *Medium impact, L.*
+9. **Pension contribution calculator** — "reach ₡X by age Y." *Low impact, S.*
+10. **Per-month recurring-item skip/override** — e.g., reduced December salary. *Low impact, S.*
+
+**Effort key:** S < 4h, one component. M = 4–16h, may need a backend endpoint. L = 16h+, schema or multi-component work.
diff --git a/codebase-review/06-weak-spots-inventory.md b/codebase-review/06-weak-spots-inventory.md
new file mode 100644
index 0000000..591b0a5
--- /dev/null
+++ b/codebase-review/06-weak-spots-inventory.md
@@ -0,0 +1,108 @@
+# Weak-Spots Inventory — Consolidated Across All Five Hats
+
+90+ raw findings from the five reviews, deduplicated into **13 themes**. Each theme lists the contributing finding IDs (traceable back to the hat reports), a consolidated severity, and the fix destination in [07-improvement-plan.md](07-improvement-plan.md).
+
+Severity reflects synthesis judgment for a *single-user, publicly exposed personal finance app* — some agent severities were adjusted (noted inline).
+
+---
+
+## Severity Matrix
+
+| # | Theme | Severity | Findings | Plan Phase |
+|---|---|---|---|---|
+| T1 | Auth & config hardening | **Critical** | SEC-01..05, SEC-08, BE-15, ARCH-01, ARCH-14 | 0 |
+| T2 | Zero test coverage | **Critical** | ARCH-06, BE-25 | 1 |
+| T3 | No real migration system | **High** | ARCH-02, BE-19 | 1 |
+| T4 | Billing-cycle math unverified | **High** | BE-06, BE-07 | 1 |
+| T5 | Money stored as float | **High** | BE-02 | 1–2 |
+| T6 | Silent failures & missing feedback (frontend) | **High** | FE-02, FE-06, FE-11, UX-01, UX-02, UX-05, UX-09, UX-15 | 3 |
+| T7 | Hand-rolled data fetching (races, no cache/cancel/timeout) | **High** | FE-01, FE-03, FE-09, FE-10, FE-18, FE-22, FE-24, ARCH-13 | 3 |
+| T8 | Ingestion trust gap (silent n8n failures, opaque dedup) | **High** | UX-17, UX-20, BE-08 | 4 |
+| T9 | Upload robustness | **Medium** | SEC-06, BE-09, BE-10, BE-14, FE-05, FE-19 | 2 |
+| T10 | Datetime & timezone hygiene | **Medium** | BE-01 | 1 |
+| T11 | Query efficiency & data-integrity constraints | **Medium** | ARCH-09, ARCH-12, ARCH-15, BE-03, BE-04, BE-05, BE-11, BE-21, BE-22, BE-23 | 2 |
+| T12 | Agent quality & safety | **Medium** | ARCH-04, ARCH-17, BE-12, BE-16, BE-17, BE-24, SEC-09, UX-16, UX-23 | 2/4 |
+| T13 | Consistency & polish (l10n, a11y, mobile, UX) | **Medium** | FE-07, FE-16, FE-17, UX-03, UX-04, UX-06, UX-07, UX-10..14, UX-18, UX-19, UX-21..25, ARCH-11 | 4 |
+
+Standalone small items: FE-08/SEC-07 (security headers → Phase 0), FE-20 (dead code → Phase 0 cleanup), FE-23 (proxy 502 → Phase 0), ARCH-05 (paste size cap → Phase 0), ARCH-16/FE-14 (response schemas/codegen → Phase 3), ARCH-18 (CSV export → Phase 4), ARCH-08/10/20 (small backend hardening → Phase 2).
+
+---
+
+## T1 — Auth & Config Hardening · Critical
+
+**The single most important theme.** The app is publicly exposed at wealth.cescalante.dev with:
+- Working fallback credentials in `config.py` (`admin/admin`, known `SECRET_KEY`) that activate silently if a deploy loses its env file — and `.env.prod` is regenerated every deploy (SEC-01, BE-15)
+- No rate limiting on login + non-constant-time comparison, against a fixed username (SEC-02, SEC-03)
+- CORS reflecting any origin with credentials (SEC-04) — currently blunted only by SameSite=Lax
+- 30-day irrevocable JWTs (SEC-05); `secure=False` cookie relying on a comment (SEC-08)
+- Hardcoded dev DB credentials in `docker-compose.yml` (ARCH-14)
+
+**Correction:** the "secrets committed to git" claim (original ARCH-01) was verified **false** — `.env*` is gitignored with no history. The theme remains Critical because of the fallback-credential failure mode, not repo leakage.
+
+**Why Critical:** every item is individually small, but together they describe a public financial app one pipeline hiccup away from `admin:admin`. Total fix effort: about half a day.
+
+## T2 — Zero Test Coverage · Critical
+
+Not one test file in the repository; pytest isn't even a dependency. The riskiest logic in the app — billing-cycle boundaries, deferred-transaction carryover, cumulative balances with manual overrides, PDF regex parsing — is guarded by nothing. Every other theme's fixes (Decimal migration, cycle verification, refactors) are unsafe to attempt without this. **Tests are the enabling investment for the entire plan.**
+
+## T3 — No Real Migration System · High
+
+Schema evolution is hand-written `ALTER TABLE` strings in `db.py:13–77` with per-statement try/except. The project's own hard rule — *never reset prod DB* (`feedback_db_reset`) — makes versioned, reversible migrations non-optional. Alembic is the obvious choice; it also unblocks T5 (Decimal) and T11 (constraints), which both need schema changes.
+
+## T4 — Billing-Cycle Math Unverified · High
+
+`get_cycle_range()` code and its own comment disagree about which 18th–18th window a month's budget covers (BE-06). Either every monthly budget is shifted by one cycle, or the comment will mislead the next change into breaking working code. Helpers also accept month 13 without complaint (BE-07). **Resolve with characterization tests against known-real statement data — first deliverable of the test suite.**
+
+## T5 — Money Stored as Float · High
+
+All monetary columns are `float` (BE-02). For an app whose entire purpose is financial accuracy, `NUMERIC`/`Decimal` is the standard. Requires: Alembic (T3) first, tests (T2) to prove equivalence, then a coordinated model + service-math migration. Highest-effort single item in the backend; schedule deliberately, not as a quick fix.
+
+## T6 — Silent Failures & Missing Feedback · High
+
+The frontend's pervasive pattern: errors → `console.error`, success → nothing. Analytics renders empty charts on API failure (FE-02); deletes/edits/uploads complete without confirmation (UX-01); upload error details returned by the backend are never shown (UX-09); CopilotKit middleware eats its own exceptions (FE-11); destructive confirmation is inconsistent (UX-15). One toast system + one standard error/retry pattern resolves the lot.
+
+## T7 — Hand-Rolled Data Fetching · High
+
+No cancellation (FE-01, FE-03), no caching/dedup (FE-10), no timeouts (FE-24), refetch-everything-on-mutation (ARCH-13), 401 redirect loop risk (FE-22), coupled effects (FE-09). **Root cause is singular: there is no query layer.** Adopting TanStack Query retires the entire theme and makes T6's error/loading states near-free. This is the highest-leverage frontend decision available.
+
+## T8 — Ingestion Trust Gap · High
+
+The app's core promise is automatic ingestion, but: n8n parse failures are invisible inside the app (UX-17); dedup decisions are reported as a bare count with no review or override (UX-20); the dedup key itself has gaps (nullable `reference`, BE-08). A user discovers missing weeks of data only at month-end. Fix in two stages: a derived Sync Status page (cheap — `max(created_at)` per source + cadence thresholds), then an import-review flow with dedup detail.
+
+## T9 — Upload Robustness · Medium
+
+Unbounded reads with no type check (SEC-06), blocking `subprocess.run` inside async routes (BE-14), temp files leaking on timeout (BE-09), per-file commits leaving half-applied batches (BE-10), frontend hanging on post-upload refetch (FE-05). One focused pass over the two upload endpoints + their UI closes all of it.
+
+## T10 — Datetime & Timezone Hygiene · Medium
+
+`datetime.utcnow()` (deprecated, naive) at 10+ sites (BE-01). Costa Rica is UTC-6; naive-UTC timestamps compared against local dates can shift late-evening transactions across cycle boundaries. Mechanical fix + a comparison audit; do it alongside T4 so the tests catch any behavior change.
+
+## T11 — Query Efficiency & Data-Integrity Constraints · Medium
+
+N+1s (ARCH-15, BE-04, BE-05), ~15 queries per projection call (BE-21), FKs without delete rules (ARCH-09/BE-11), missing tiebreaker ordering (ARCH-12), check-then-insert race (BE-22), non-unique account labels (BE-23), non-atomic savings accrual (BE-03). None urgent for single-user load; all cheap; constraints need Alembic (T3) first.
+
+## T12 — Agent Quality & Safety · Medium
+
+Tools duplicate service logic so UI and assistant can drift (ARCH-04); hardcoded fallback rates make it confidently wrong when the rate service is down (BE-24); no year bounds (BE-16); cryptic failure when called out of context (BE-12); fixed limits hide older history (BE-17). Safety posture is acceptable *because tools are read-only* — keep that as an explicit invariant (SEC-09). UX: cards are ephemeral and nothing is exportable (UX-16, UX-23).
+
+## T13 — Consistency & Polish · Medium
+
+The long tail that makes the app feel finished: three month-navigation patterns (UX-03), three month-name implementations in two languages (UX-19, FE-17, UX-07), inert search on cash tab (UX-04), no-undo deletes (UX-10), dense mobile tables (UX-11), hardcoded pension assumptions (UX-12), cycle filter applying to one of three charts (UX-13), dead-end empty states (UX-14), deferred-transaction UI gap (ARCH-11, UX-05), a11y gaps on toggles (FE-16), meter-ID legends (UX-24). Individually small; batch them into themed polish passes.
+
+---
+
+## Cross-Hat Agreement (highest-confidence signals)
+
+Where independent hats converged on the same problem, confidence is highest:
+
+| Problem | Flagged independently by |
+|---|---|
+| Default credentials / weak config | Architect, Backend, Cybersec |
+| No tests | Architect, Backend |
+| No migrations | Architect, Backend |
+| Upload validation/robustness | Backend, Cybersec, Frontend, Poweruser |
+| Silent error swallowing | Frontend, Poweruser, Backend (exchange-rate `pass`) |
+| Agent/service logic duplication | Architect, Backend |
+| Localization inconsistency | Frontend, Poweruser |
+| Deferred-transaction UI gap | Architect, Poweruser |
+| Refetch-everything / no query layer | Architect, Frontend |
diff --git a/codebase-review/07-improvement-plan.md b/codebase-review/07-improvement-plan.md
new file mode 100644
index 0000000..48bd413
--- /dev/null
+++ b/codebase-review/07-improvement-plan.md
@@ -0,0 +1,203 @@
+# Improvement Plan — WealthySmart
+
+Phased plan derived from [06-weak-spots-inventory.md](06-weak-spots-inventory.md). Phases are ordered by dependency and risk: **harden → build foundations → fix correctness → rebuild the frontend data layer → polish and grow**. Each phase is independently shippable; stop-anywhere is safe.
+
+Conventions respected throughout: small logical commits, push to GitHub origin (Gitea mirrors), `.env.prod` vars must be added in **both** Gitea secrets and the workflow (see `reference_gitea_deploy`), never reset the prod DB.
+
+---
+
+## Phase 0 — Security Hardening Sprint
+
+**Goal:** close every cheap hole in the public-facing surface. **Effort: ~1 day.** No schema changes, no behavior changes for the legitimate user (except needing real env values — verify Gitea secrets are complete *before* deploying).
+
+### 0.1 Fail-fast configuration (SEC-01, BE-15)
+- `backend/app/config.py`: remove defaults for `SECRET_KEY`, `ADMIN_USERNAME`, `ADMIN_PASSWORD`; add a pydantic `model_validator` rejecting empty/known-default values.
+- Keep a documented `.env.example` (no real values) as the template.
+- **Pre-deploy check:** confirm all three exist in Gitea secrets and the workflow env-file generation step.
+- *Accept:* app refuses to boot with missing/default secrets; prod deploy still succeeds.
+
+### 0.2 Login hardening (SEC-02, SEC-03)
+- Consolidate to a single login implementation (the cookie login in `main.py` is the one the SPA uses; have `endpoints/auth.py` delegate or remove it from the router if unused).
+- `hmac.compare_digest` on username and password.
+- Rate limit: `slowapi` 5/min/IP on login (in-memory is fine, single process). Return 429 with a Retry-After.
+- *Accept:* 6th attempt within a minute → 429; valid login unaffected.
+
+### 0.3 CORS pinning (SEC-04)
+- `allow_origins=["https://wealth.cescalante.dev", "http://localhost:3000", "http://localhost:3001"]`; explicit methods/headers.
+- *Accept:* prod SPA, dev SPA, and n8n flows (server-to-server — CORS-exempt) all still work.
+
+### 0.4 Token & cookie tightening (SEC-05, SEC-08)
+- `ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7` (7 days — balance for a personal app).
+- New `COOKIE_SECURE: bool = True` setting (False in dev env); use it in `set_cookie`. Add to Gitea secrets/workflow if env-driven.
+- *Accept:* prod cookie has `Secure`; dev login still works over http.
+
+### 0.5 Security headers (SEC-07, FE-08)
+- Hono middleware in `frontend/server.ts`: `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`. CSP deferred until inline-script usage is audited (CopilotKit may need allowances).
+- HSTS at nginx-proxy level (VPS config, not repo).
+
+### 0.6 Small backend/proxy guards (ARCH-05, FE-23, FE-22)
+- `max_length` on paste-import request text.
+- try/catch → 502 JSON in `proxyToBackend`.
+- Skip 401 redirect when already on `/login` in `api.ts`.
+
+### 0.7 Housekeeping
+- Delete dead `AgentHomeClient.tsx` (FE-20).
+- Move dev DB credentials from `docker-compose.yml` into `.env` references (ARCH-14).
+- Rotate any secret previously pasted into logs/chats as a precaution.
+
+---
+
+## Phase 1 — Foundations: Tests, Migrations, Time
+
+**Goal:** make every later change safe. **Effort: ~1 week.** This phase has the highest leverage in the plan.
+
+### 1.1 Test harness (ARCH-06, BE-25)
+- Add `pytest`, `httpx` (test client) to a new `requirements-dev.txt`; create `backend/tests/` with a session fixture (SQLite in-memory or dockerized Postgres — prefer Postgres for `DISTINCT ON`/`NUMERIC` fidelity).
+- CI: extend the Gitea workflow with a test job that gates deploy; add `pnpm typecheck` for the frontend.
+- *Accept:* `pytest` green locally and in CI; deploy blocked on red.
+
+### 1.2 Characterization tests for cycle math — **before touching the code** (BE-06, BE-07, T4)
+- `test_budget_projection.py`: pin current behavior of `get_cycle_range`/`get_month_range` for: mid-month dates, the 18th itself, month/year boundaries, Feb 28/29.
+- Validate against **known-real data**: pick 2–3 historical months where the correct BAC statement total is known and assert the projection matches.
+- Then resolve the code/comment contradiction — fix whichever is wrong; add input validation (`1 <= month <= 12`, MIN/MAX_YEAR).
+- *Accept:* documented, test-pinned answer to "which cycle does month M's budget use," verified against a real statement.
+
+### 1.3 Alembic (ARCH-02, BE-19, T3)
+- `alembic init`; baseline autogenerated from current models; `alembic stamp head` against the existing prod schema (no destructive ops).
+- Port the `run_migrations()` SQL into history as already-applied; reduce `db.py` startup to `alembic upgrade head`.
+- Document the workflow in CLAUDE.md (`alembic revision --autogenerate` → review → commit).
+- *Accept:* fresh dev DB builds from migrations alone; prod stamps cleanly with zero data change.
+
+### 1.4 Datetime sweep (BE-01, T10)
+- Replace `datetime.utcnow()` → `datetime.now(timezone.utc)`; audit each comparison for naive/aware mixing (token expiry in `auth.py`, rate cache, budget date filters).
+- Add a test for token expiry comparison.
+- *Accept:* zero `utcnow` references; tests green.
+
+### 1.5 Decimal migration — staged start (BE-02, T5)
+- This phase only: write equivalence tests for projection math (current float behavior) so Phase 2's migration has a baseline. The actual column migration happens in Phase 2 after Alembic is proven in prod once.
+
+---
+
+## Phase 2 — Backend Correctness & Robustness
+
+**Goal:** the backend tells the truth and fails loudly. **Effort: ~1 week.**
+
+### 2.1 Money → Decimal (BE-02)
+- Alembic migration: monetary columns → `NUMERIC(15,2)` (rates → `NUMERIC(15,6)`); models → `Decimal`; sweep `budget_projection.py` / `exchange_rate.py` / agent tools for float arithmetic; ensure JSON serialization keeps current shape (FastAPI serializes Decimal → number/string; pick and pin one — test it).
+- *Accept:* Phase 1.5 equivalence tests pass within tolerance; API responses unchanged in shape; prod migration applied without reset.
+
+### 2.2 Upload pipeline hardening (SEC-06, BE-09, BE-10, BE-14 — T9)
+- Both upload endpoints: 10 MB bounded read, `%PDF` magic check, `asyncio.to_thread()` around parsing, `TemporaryDirectory()` for cleanup.
+- Keep per-file commits but return a precise per-file manifest (filename → imported/updated/skipped/error) — pairs with UX-09 in Phase 3.
+- *Accept:* oversized/non-PDF rejected with clear errors; event loop unblocked during parse (verify with a concurrent request); no temp files left after a forced timeout.
+
+### 2.3 Query & constraint pass (T11)
+- N+1 fixes: category prefetch (ARCH-15), `DISTINCT ON` pensions (BE-04), eager-loaded water readings (BE-05); collapse projection aggregates into grouped queries (BE-21).
+- Alembic migration: FK delete rules — `SET NULL` for `Transaction.category_id` / `RecurringItem.category_id`, `CASCADE` for `WaterMeterReading.receipt_id` (ARCH-09, BE-11); pagination tiebreaker `, id DESC` (ARCH-12); `ON CONFLICT DO UPDATE` for pension upsert (BE-22).
+- Atomic savings accrual: raise early on missing accounts (BE-03).
+- *Accept:* projection endpoint query count measured before/after; constraint behavior covered by tests.
+
+### 2.4 Error-visibility pass (BE-18, BE-12, ARCH-08)
+- `exchange_rate.py`: specific exceptions + `logger.warning` per source; timestamp on `_last_known` caches with freshness in responses (BE-20).
+- Clear `RuntimeError` for unbound agent session; explicit `CancelledError` handling in the refresh loop.
+
+### 2.5 Agent tool cleanup (T12 backend half)
+- Tools delegate to service functions (ARCH-04); year bounds (BE-16); remove `600.0`/`1.08` magic fallbacks — use last persisted DB rate or state unavailability (BE-24); optional offset params (BE-17).
+- Document the invariant: **agent tools are read-only**; any future write tool requires a UI confirmation step (SEC-09).
+- *Accept:* assistant's cycle summary numerically matches the Budget page for the same month (test).
+
+### 2.6 Consistency sweep (ARCH-03, ARCH-10, ARCH-20)
+- Enum members over string literals; keyword `status_code=`; `SimpleCookie` parsing; break the auth/db lazy-import cycle.
+
+---
+
+## Phase 3 — Frontend Data Layer & Feedback
+
+**Goal:** kill the silent-failure class wholesale. **Effort: ~1 week.**
+
+### 3.1 Adopt TanStack Query (FE-10 → retires FE-01/03/09/18/24, ARCH-13 — T7)
+- `QueryClientProvider` at the root; wrap the existing `api.ts` request helper (keep `ApiError`, 401 handling); default `AbortSignal.timeout(30_000)`, `staleTime` ~30s, one retry.
+- Migrate page by page: **Budget first** (worst race conditions), then Analytics, Pensions, Salarios, ServiciosMunicipales. Replace `useBudget`'s manual fetch trio with queries + targeted mutation invalidation.
+- *Accept:* rapid month/source/search changes never render stale data; switching pages back within 30s renders instantly from cache.
+
+### 3.2 Toast + error-state standard (T6: UX-01, UX-02, FE-02, FE-06, UX-09, UX-15)
+- Add Sonner; success/error toast on every mutation; shared `` (or per-page pattern) rendering skeleton → error-with-retry → data/empty.
+- Render upload `errors[]` from the Phase 2.2 manifest in an Alert list.
+- ConfirmDialog on *all* destructive actions including balance-override clear.
+- *Accept:* with the backend stopped, every page shows an explicit error with retry; every mutation produces visible confirmation.
+
+### 3.3 Auth-flow fixes (FE-15, FE-22)
+- Auth probe distinguishes network failure (retry once / unknown state) from 401; redirect guard from 0.6 verified.
+
+### 3.4 Type alignment (ARCH-16 + FE-14)
+- Backend: shared Pydantic response models + `response_model=` on every route.
+- Frontend: generate types from OpenAPI (`openapi-typescript`) replacing hand-written interfaces in `api.ts`; CI check that generated types are current.
+- *Accept:* a backend schema change breaks `pnpm typecheck` instead of production rendering.
+
+### 3.5 Small fixes batch
+- Lazy initializers for theme/privacy (FE-07); guard Pensions ROI indexing (FE-04); timeout post-upload refetch (FE-05); cap upload-result list (FE-19); log CopilotKit middleware errors (FE-11); month-detail existence check (FE-12).
+
+---
+
+## Phase 4 — UX, Trust & Features
+
+**Goal:** the daily-use payoff. Ordered by impact; each item independent. **Effort: 2–3 weeks spread.**
+
+### 4.1 Sync Status page (UX-17 — the #1 trust feature)
+- v1 (M): backend endpoint deriving per-source health from existing data — `max(created_at)` per `TransactionSource` + pension/municipal uploads; cadence thresholds (BAC ≈ daily-weekly, salary biweekly/monthly, municipal/pension monthly) → OK/warning per source. Frontend page + sidebar badge on warning.
+- v2 (L): n8n execution-status integration (n8n DB is already queryable per CLAUDE.md) for actual failure detail.
+- *Accept:* stop an n8n flow for N days → visible warning in-app.
+
+### 4.2 Import review & dedup transparency (UX-20, BE-08)
+- Import responses list skipped duplicates (incoming row + matched existing row); UI renders the comparison with "import anyway" override; tighten the reference-hash inputs (include `card_last4`).
+- *Accept:* paste-import of a file with near-duplicates shows exactly what was skipped and why; override works.
+
+### 4.3 Localization & formatting unification (UX-19, FE-17, UX-07 — T13)
+- Single `src/lib/dates.ts` using `Intl` with `es-CR`; delete the three per-page month-name arrays; sweep `en-US` usages; translate stray English labels.
+- *Accept:* `grep -r "en-US" src/` returns nothing; one source of truth for month names.
+
+### 4.4 Transactions power features (Wishlist #1, #2; UX-04)
+- Search across all sources (fix the inert cash-tab search first — S); backend `q` param searching merchant/notes/reference; filter bar (date range, amount range, category multiselect); multi-select with bulk re-categorize / defer / delete-with-undo.
+
+### 4.5 Undo & destructive-action safety (UX-10)
+- Toast-with-Undo (5s delayed API call) for transaction and recurring-item deletes — no schema change needed.
+
+### 4.6 Mobile table experience (UX-11)
+- Card-stack rendering under 640px for the transaction DataTable (merchant/amount primary; category/bank secondary).
+
+### 4.7 Module polish batch (each S–M)
+- Shared month/period navigator across Budget/Proyecciones/Analytics; Pensions history navigation (UX-03)
+- Deferred-transaction visibility: modal field + row styling + toast (ARCH-11, UX-05)
+- Editable pension assumptions persisted server-side (UX-12)
+- Cycle filter on all three Analytics charts (UX-13)
+- Empty states with "add first item" actions (UX-14); dirty-form guard (UX-06); balance-override helper text (UX-21); breadcrumb from Proyecciones (UX-22); meter labels (UX-24); a11y `aria-pressed` sweep (FE-16); scroll-container fix (UX-25)
+
+### 4.8 Export & assistant value (ARCH-18, UX-16, UX-23)
+- CSV export endpoint + buttons for transactions/salarios/pensions; copy/export on `SpendingSummaryCard`.
+
+### 4.9 Wishlist backlog (unscheduled)
+- What-if scenario planner · exchange-rate history & per-date override · PWA polish · pension contribution calculator · per-month recurring-item overrides.
+
+---
+
+## Sequencing & Dependencies
+
+```
+Phase 0 (1 day) ──► Phase 1 (1 wk) ──► Phase 2 (1 wk) ──► Phase 3 (1 wk) ──► Phase 4 (2–3 wks)
+ security tests ◄─────────── needs tests needs 2.2 manifest needs 3.x for
+ (no deps) alembic ◄────────── needs alembic (for UX-09) error patterns
+ cycle truth decimal, uploads, 4.1/4.2 need
+ datetime constraints, agent backend endpoints
+```
+
+Hard dependencies: **1.2 before any budget-math edit** · **1.3 before any schema change (2.1, 2.3, 4.2)** · **1.1 before 2.1** · **3.1 before 3.2** (error states come from the query layer) · **2.2 before the UX-09 part of 3.2**.
+
+Parallelizable: Phase 0 anytime; 4.3 (localization) anytime; 3.x frontend work can overlap 2.x backend work except where noted.
+
+## Definition of Done (whole plan)
+
+- App refuses to boot with default secrets; login rate-limited; CORS pinned; uploads validated.
+- `pytest` + `pnpm typecheck` gate deploys; cycle math is test-pinned against real statements.
+- Schema changes ship as Alembic migrations only; money is `NUMERIC`/`Decimal` end-to-end.
+- No silent failures: every page has error+retry; every mutation has visible feedback; ingestion health is observable in-app.
+- One locale, one date formatter, one month-navigation pattern.
diff --git a/codebase-review/README.md b/codebase-review/README.md
new file mode 100644
index 0000000..24d630c
--- /dev/null
+++ b/codebase-review/README.md
@@ -0,0 +1,45 @@
+# WealthySmart — Five-Hat Codebase Review
+
+**Date:** 2026-06-09
+**Scope:** Full repository (`backend/`, `frontend/`, compose files, CI workflow). `tech_docs/`, `node_modules/`, and `.claude/` excluded.
+**Method:** Five independent senior/staff-level review agents, each with a distinct lens, followed by cross-verification of the highest-severity claims against the actual repo state and a consolidated synthesis.
+
+## The Five Hats
+
+| Hat | Report | Focus |
+|---|---|---|
+| 🏛 Architect | [01-architect.md](01-architect.md) | System structure, layering, data model, migrations, deploy topology |
+| ⚙️ Backend | [02-backend.md](02-backend.md) | FastAPI/SQLModel correctness, money/date math, queries, PDF parsing, agent tools |
+| 🎨 Frontend | [03-frontend.md](03-frontend.md) | React data fetching, state, type safety, a11y, Hono server, CopilotKit |
+| 🔐 Cybersec | [04-security.md](04-security.md) | AuthN/Z, secrets, CORS, rate limiting, uploads, injection surface |
+| 👤 Poweruser | [05-poweruser.md](05-poweruser.md) | Daily-use UX, feedback loops, data trust, consistency, feature gaps |
+
+## Synthesis Documents
+
+- **[06-weak-spots-inventory.md](06-weak-spots-inventory.md)** — All 90+ findings deduplicated into 13 cross-cutting themes with a severity matrix.
+- **[07-improvement-plan.md](07-improvement-plan.md)** — Phased implementation plan (Phase 0–4) with concrete steps, acceptance criteria, and effort estimates.
+
+## Editorial Corrections (verified against the repo)
+
+Agent findings were spot-checked. One material correction:
+
+- **`.env` / `.env.prod` are NOT committed to git.** Both are covered by `.gitignore` (`.env`, `.env.*`) and `git log --all --diff-filter=A -- .env .env.prod` returns nothing. The Architect's ARCH-01 ("secrets in version control", Critical) is **downgraded**: the files exist only in the working tree, which is expected. The *real* residual risks are (a) hardcoded fallback credentials in `backend/app/config.py` (verified: `SECRET_KEY = "change-me-in-production"`, `ADMIN_PASSWORD = "admin"`) and (b) hardcoded dev DB credentials in `docker-compose.yml`. These are tracked as SEC-01 / BE-15.
+
+Claims that were verified as accurate: CORS `allow_origins=["*"]` with `allow_credentials=True` (`backend/app/main.py:81`), 30-day token expiry (`backend/app/config.py:7`), default admin credentials (`backend/app/config.py:9`), zero test files in the repo, and no Alembic directory.
+
+## Top 10 — If You Only Fix Ten Things
+
+1. **Fail fast on default secrets** — startup must refuse to run with `SECRET_KEY="change-me-in-production"` or `admin/admin` (SEC-01, BE-15). *S*
+2. **Rate-limit + constant-time login** — both login endpoints have unlimited attempts and `!=` comparison (SEC-02, SEC-03). *S*
+3. **Restrict CORS** to `https://wealth.cescalante.dev` (SEC-04). *S*
+4. **Add a test suite** — zero tests guard the most complex logic in the app (billing-cycle math, deferred carryover, PDF parsing) (ARCH-06, BE-25). *L*
+5. **Adopt Alembic** — schema evolution is ad-hoc `ALTER TABLE` strings in `db.py`; memory says "never reset prod DB," which makes real migrations essential (ARCH-02, BE-19). *M*
+6. **Verify billing-cycle boundary math** — possible off-by-one in `get_cycle_range` would misalign every monthly budget (BE-06). *S to verify, with tests*
+7. **Migrate money fields from `float` to `Decimal`/`Numeric`** (BE-02). *M*
+8. **Validate uploads** — size cap, `%PDF` magic-byte check, per-file transactional rollback (SEC-06, BE-10). *S*
+9. **Frontend error/feedback layer** — surfaced errors, success toasts, request cancellation; today failures are silent `console.error` (FE-01/02/03, UX-01/02). *M*
+10. **n8n sync visibility** — silent parser failures mean missing transactions go unnoticed; add a sync-status/reconciliation view (UX-17, UX-20). *L*
+
+## Reading Order
+
+For a quick pass: this README → [06-weak-spots-inventory.md](06-weak-spots-inventory.md) → [07-improvement-plan.md](07-improvement-plan.md). The five hat reports hold the file:line evidence behind every consolidated item.