14 KiB
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 forSECRET_KEY,ADMIN_USERNAME,ADMIN_PASSWORD; add a pydanticmodel_validatorrejecting 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.pyis the one the SPA uses; haveendpoints/auth.pydelegate or remove it from the router if unused). hmac.compare_digeston username and password.- Rate limit:
slowapi5/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 = Truesetting (False in dev env); use it inset_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_lengthon paste-import request text.- try/catch → 502 JSON in
proxyToBackend. - Skip 401 redirect when already on
/logininapi.ts.
0.7 Housekeeping
- Delete dead
AgentHomeClient.tsx(FE-20). - Move dev DB credentials from
docker-compose.ymlinto.envreferences (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 newrequirements-dev.txt; createbackend/tests/with a session fixture (SQLite in-memory or dockerized Postgres — prefer Postgres forDISTINCT ON/NUMERICfidelity). - CI: extend the Gitea workflow with a test job that gates deploy; add
pnpm typecheckfor the frontend. - Accept:
pytestgreen 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 ofget_cycle_range/get_month_rangefor: 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 headagainst the existing prod schema (no destructive ops).- Port the
run_migrations()SQL into history as already-applied; reducedb.pystartup toalembic 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 inauth.py, rate cache, budget date filters). - Add a test for token expiry comparison.
- Accept: zero
utcnowreferences; 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; sweepbudget_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,
%PDFmagic 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 ONpensions (BE-04), eager-loaded water readings (BE-05); collapse projection aggregates into grouped queries (BE-21). - Alembic migration: FK delete rules —
SET NULLforTransaction.category_id/RecurringItem.category_id,CASCADEforWaterMeterReading.receipt_id(ARCH-09, BE-11); pagination tiebreaker, id DESC(ARCH-12);ON CONFLICT DO UPDATEfor 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.warningper source; timestamp on_last_knowncaches with freshness in responses (BE-20).- Clear
RuntimeErrorfor unbound agent session; explicitCancelledErrorhandling 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.08magic 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=;SimpleCookieparsing; 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)
QueryClientProviderat the root; wrap the existingapi.tsrequest helper (keepApiError, 401 handling); defaultAbortSignal.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 inapi.ts; CI check that generated types are current. - Accept: a backend schema change breaks
pnpm typecheckinstead 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)perTransactionSource+ 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.tsusingIntlwithes-CR; delete the three per-page month-name arrays; sweepen-USusages; 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
qparam 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-pressedsweep (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 typecheckgate deploys; cycle math is test-pinned against real statements.- Schema changes ship as Alembic migrations only; money is
NUMERIC/Decimalend-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.