Commit Graph

14 Commits

Author SHA1 Message Date
Carlos Escalante
700e1edbb4 Assistant: stop stale clients from resurrecting cleared threads
All checks were successful
Deploy to VPS / test (push) Successful in 1m37s
Deploy to VPS / deploy (push) Successful in 28s
Root cause of the "chunks" clear bug: conversation history is
server-owned, but MAF seeds a thread from whatever the client sends
when no snapshot exists. Any stale tab (open across a clear or from an
older session) still holds the old conversation in memory, and its next
question re-persisted the whole thing — clears appeared to peel off one
layer at a time.

The agent middleware now drops client-sent history for threads with no
stored snapshot, keeping only the last user turn (fresh conversations
and empty-messages hydration requests pass through untouched). Verified
live: simulated a cleared-thread stale tab, asked from it, and only the
new turn was persisted.

Also: clearThread surfaces DELETE failures as a toast instead of
silently reloading, and conftest sets a dummy OPENAI_API_KEY so tests
can import app.main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 20:06:35 -06:00
Carlos Escalante
7b7c741ba2 Agent: resolve "today" in the browser's timezone, not hardcoded CR
The CR-hardcoded today broke two scenarios: the owner traveling, and
any future non-CR user. Now the provider sends the browser's IANA
timezone (X-Client-Timezone) with every CopilotKit request — the
runtime forwards x-* headers to the agent on its own — and the backend
binds it to a per-request ContextVar next to the DB session.
get_current_date and the future-cuota bounds use it; the tool also
reports the timezone so the model can echo it. Unknown or absent zones
(n8n posts, tests) fall back to Costa Rica; comma-joined duplicate
header values are tolerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 19:33:27 -06:00
Carlos Escalante
965e30a2d2 Agent: add get_daily_spending for day-scoped questions
All checks were successful
Deploy to VPS / test (push) Successful in 1m32s
Deploy to VPS / deploy (push) Successful in 14s
Prod showed the failure mode after the date fix: get_current_date
returned the right day, but the model answered "¿cuánto he gastado
hoy?" from cycle-level aggregates (the only spend tools it had) and
concluded ₡0. Give it a day-grained tool — per-day CRC totals mirroring
/analytics/daily-spending (COMPRA only, no installment anchors, no
future cuotas) — and a prompt rule to match tool to time grain.
Verified: fresh thread now calls get_current_date →
get_daily_spending(2026-07-04, 2026-07-05) and answers correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:36:48 -06:00
Carlos Escalante
334d41bd9a Agent: fix date handling — Costa Rica timezone and future cuotas
All checks were successful
Deploy to VPS / test (push) Successful in 1m32s
Deploy to VPS / deploy (push) Successful in 14s
Two bugs from prod (server clock is UTC, user is UTC-6):

- get_recent_transactions returned future-dated Tasa Cero cuotas; it now
  excludes anything dated after the user's today, same rule as
  /transactions/recent.
- "¿Cuánto he gastado hoy?" answered for the wrong day: the prompt baked
  date.today() in at boot — frozen from startup AND in server-UTC (8 pm
  in Costa Rica is already tomorrow in UTC). The prompt no longer carries
  a date; a new get_current_date tool returns today in CR time plus the
  active billing cycle, and the prompt directs the model to call it for
  any relative date reference. today_cr() lives in timeutil (CR has no
  DST, fixed UTC-6).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:27:46 -06:00
Carlos Escalante
6bce5539f7 Assistant: persist the chat thread in Postgres
Back MAF's opt-in AG-UI snapshot persistence with a DB store so the
conversation survives page reloads and restarts:

- ChatThreadSnapshot: one upserted row per (scope, thread_id), JSONB
  messages/state/interrupt (migration 794baf50635b)
- PostgresSnapshotStore implements the AGUIThreadSnapshotStore protocol.
  It opens its own sessions (MAF saves during SSE streaming, after the
  request middleware has closed the ContextVar session) and sanitizes on
  save: camelCase toolCalls/toolCallId (the @ag-ui/client Zod schema
  strips snake_case, which would lose cards on replay), drop BFF ctx-*
  context messages, dedupe MAF's re-recorded multi-run turns, and drop
  orphaned tool results
- Endpoint mount gains snapshot_store + a constant scope resolver
  (single-user app; auth already enforced by agent_auth_and_session)
- DELETE /api/v1/chat/thread resets the conversation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:09:55 -06:00
Carlos Escalante
ce7073cf24 Analytics: date-range filter + fix category endpoint Decimal crash
by-category and daily-spending accept start_date/end_date (range wins
over cycle); Analytics gets a two-month calendar range picker beside
the cycle selector. Fixes a latent float/Decimal TypeError that 500'd
spending_by_category whenever it had data — the category donut had
never rendered. Regression + range tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:06:58 -06:00
Carlos Escalante
3cd2927b5f Tasa Cero: backend tests (rounding, scheduling, exclusion, lifecycle)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 16:13:00 -06:00
Carlos Escalante
ce3cc69846 Import dedup transparency + override; bulk transaction actions
Paste-import now returns each skipped duplicate WITH the matched
existing row (id, merchant, date, amount, source) instead of a bare
count, and accepts allow_duplicates to force-import false positives —
the practical fix for same-day identical purchases hashing together
(UX-20; supersedes the BE-08 hash change, which would have orphaned
every existing hash). POST /transactions/bulk applies
delete/set_category/set_deferred to up to 500 ids atomically. 82 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 20:46:23 -06:00
Carlos Escalante
4ceb67564a Sync Status: per-source ingestion health page with nav warning badge
The n8n flows fail invisibly from the app's perspective (review UX-17,
the top trust gap). /api/v1/sync-status derives last-received + cadence
thresholds for BAC, salary, municipal, pensions and exchange rate from
existing data — no n8n integration needed. New Sincronización page
shows per-source freshness and a hint to check n8n; the sidebar item
gets an amber dot when any source is stale. Verified live against the
dev DB (correctly flags its stale BAC data at 43 days).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 18:03:55 -06:00
Carlos Escalante
82f10a5d7c Error visibility and agent tool cleanup
Exchange-rate fetchers log every source failure instead of silently
passing (BE-18); the rate tool exposes fetched_at so staleness is
visible (BE-20). Agent: unbound-session failures raise a clear
RuntimeError (BE-12); net worth converts via get_crc_multipliers with
last-known fallbacks instead of a hardcoded 600 CRC / 1.08 EUR guess,
and reports accounts it cannot convert rather than inventing numbers
(BE-24); budget tool enforces MIN/MAX_YEAR (BE-16); municipal receipts
gain offset paging (BE-17); category analytics prefetches names; the
module docstring pins the read-only tool policy (SEC-09). Note: the
refresh loop never swallowed CancelledError (BaseException since 3.8) —
ARCH-08 was a false positive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:23:21 -06:00
Carlos Escalante
82a7bfc4c5 Harden PDF uploads: size cap, magic-byte check, non-blocking parse
read_pdf_upload() bounds reads at 10 MB and requires the %PDF magic
before anything reaches pdftotext (SEC-06); rejections land in the
per-file errors list with the filename. Parsing now runs via
asyncio.to_thread so a slow PDF no longer blocks the event loop for up
to 30s (BE-14). Review notes: temp files were already context-managed
(BE-09 overstated) and the pension batch already commits atomically
(BE-10 overstated). 69 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:00:49 -06:00
Carlos Escalante
3cdd7d9eab Constraint and query pass: FK delete rules, grouped queries, tiebreakers
- FK rules via migration 7884505b16b3 (verified on dev with a rolled-
  back probe): transactions/recurring items SET NULL on category delete
  (this also fixes the 500 that DELETE /categories raised for in-use
  categories), water readings CASCADE with their receipt. Models declare
  ondelete to stay in sync. (ARCH-09, BE-11)
- compute_actuals_by_source: 3 grouped queries replace 10 single-
  aggregate round-trips; behavior pinned by the existing cycle suite.
  (BE-21)
- compute_cc_by_category prefetches category names (ARCH-15); agent
  pension latest-per-fund resolved in SQL (BE-04); municipal water
  consumption batched into one grouped query (BE-05).
- Deterministic pagination: date DESC, id DESC on all transaction
  listings. (ARCH-12)
- New agent-tool tests (session binding, JSON-safe output, batched
  queries): 64 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:45:46 -06:00
Carlos Escalante
74886fbf98 Migrate all 29 money columns from float to NUMERIC/Decimal (BE-02)
Models use Money = Annotated[Decimal, PlainSerializer(float)] so storage
is exact NUMERIC(15,2) (rates 15,6) while JSON keeps emitting plain
numbers — the frontend contract is unchanged, pinned by
test_models_serialization.py. Service layers coerce to float at their
boundaries (projection math, rate multipliers, agent tool output, the
savings constants become Decimal) so no naive Decimal/float mixing can
raise. Migration 7f567c8bafdb applied to dev: per-row values and the
table sum byte-identical before/after; endpoints verified live on the
new schema. 59/59 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:38:28 -06:00
Carlos Escalante
c725c1487d Add pytest harness and 55-test suite for core backend logic
Covers: billing-cycle characterization (the BE-06 convention, boundary
instants, year wraps, deferred transactions), projection engine
(recurring schedules, no-double-count, cumulative balances, overrides,
fresh start), auth (JWT roundtrip/expiry, API tokens, constant-time
creds, rate-limit window), fail-fast settings, and the BAC paste
parser. SQLite in-memory fixtures, exchange rates pinned — no network.
Also doubles as the float-behavior baseline for the Phase 2 Decimal
migration (plan 1.5). config.py moves to SettingsConfigDict to clear
the pydantic deprecation warning from test output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:46:54 -06:00