Files
WealthySmart/codebase-review/07-improvement-plan.md
Carlos Escalante 5c265129e8 Add five-hat codebase review and phased improvement plan
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:13:19 -06:00

14 KiB
Raw Blame History

Improvement Plan — WealthySmart

Phased plan derived from 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.
  • 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 23 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 <QueryBoundary> (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: 23 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 SM)

  • 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 (23 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.