Commit Graph

133 Commits

Author SHA1 Message Date
Carlos Escalante
440da3e394 Consistency sweep: enum members, keyword status codes, SimpleCookie
analytics.py filters by TransactionType.COMPRA instead of the string
literal; budget.py uses keyword HTTPException args; the agent
middleware parses cookies with stdlib SimpleCookie instead of a regex
(ARCH-10); auth.py's lazy imports are hoisted — there was never an
actual auth/db cycle (ARCH-20); transactions.py hoists or_/and_.
Verified: dev container boots, agent auth paths return 401 for
missing/garbage cookies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 15:23:21 -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
881d879ed1 Gate deploys on backend tests and frontend typecheck; record Phase 1
All checks were successful
Deploy to VPS / test (push) Successful in 1m51s
Deploy to VPS / deploy (push) Successful in 1m4s
The new test job runs the 55-test pytest suite and pnpm typecheck
before the deploy job (needs: test) — a red suite now blocks prod.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 20:59:34 -06:00
Carlos Escalante
49b96ed49c Fix all 20 pre-existing type errors; typecheck now green
- ui/chart.tsx: recharts 3 stopped exposing tooltip/legend content props
  on its public types; declare the ChartPayloadItem shape we consume
- base-ui Select onValueChange passes string|null: guard null in
  BillingCycleSelector, PasteImportModal, TransactionModal
- type the untyped api.get calls (cycles, categories)
- push-notifications: back the VAPID key with an explicit ArrayBuffer
  so it satisfies BufferSource under TS 5.9

Unblocks gating CI on pnpm typecheck.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 20:55:07 -06:00
Carlos Escalante
9a25c4baab Adopt Alembic: autogenerated baseline, stamp-or-upgrade startup
The baseline (c3da001a0eb3) was autogenerated against an empty Postgres
and schema-diffed against the live dev schema until column-identical
(two model fixes fell out: deferred_to_next_cycle now declares its
server_default, usersettings.data is JSONB on Postgres to match the
live type). Startup replaces init_db/run_migrations with
run_alembic_upgrade(): pre-Alembic databases that already match the
baseline are adopted via stamp; fresh databases build from the
migration. The section is serialized with a pg advisory lock because
prod uvicorn runs 2 workers, each executing the lifespan. Verified:
adoption + idempotent re-run on the dev DB, fresh 13-table build on an
empty DB, full container boot, 55/55 tests. (ARCH-02, BE-19)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 20:31:55 -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
Carlos Escalante
996bd437a1 Replace deprecated datetime.utcnow with shared naive-UTC helper
All 29 call sites now use app.timeutil.utcnow(), which returns naive
UTC via the non-deprecated API. Semantics are unchanged on purpose:
every datetime column is TIMESTAMP WITHOUT TIME ZONE, so going aware
here would poison naive/aware comparisons. The TIMESTAMPTZ migration
(Phase 2) now has a single place to change. (BE-01)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:46:54 -06:00
Carlos Escalante
0a4c3e9436 Validate month input and document cycle labeling convention
Review finding BE-06 (suspected off-by-one in get_cycle_range) is a
FALSE POSITIVE: budget month M composes get_previous_cycle with
get_cycle_range to cover [(M-1)/18, M/18), matching both the code
comment and the frontend convention. Verified against prod data: May
2026 cycle-window SQL sum matches the API (95 txs exactly, refunds to
the colón, 0.13%% delta from one EUR tx). The docstring now spells out
the labeling convention, and month is validated 1-12 (BE-07).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:46:54 -06:00
Carlos Escalante
fa19ac1988 Document Phase 0 completion in codebase-review/PROGRESS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:34:48 -06:00
Carlos Escalante
d874cf540d Fix CI frontend build: pin pnpm, approve dependency build scripts
All checks were successful
Deploy to VPS / deploy (push) Successful in 2m41s
The Docker build's unpinned corepack resolved a newer pnpm that hard-
errors on unreviewed dependency build scripts (ERR_PNPM_IGNORED_BUILDS),
failing deploy task 80. Pin packageManager to the locally-verified
pnpm 10.33.1 and review the build scripts: @swc/core and esbuild are
approved (vite needs their native binaries), @scarf/scarf is ignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:22:57 -06:00
Carlos Escalante
3e56f1ca66 Fix type errors in Analytics and Budget fetch params
Some checks failed
Deploy to VPS / deploy (push) Failing after 38s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:13:32 -06:00
Carlos Escalante
4a758b29a7 Remove dead AgentHomeClient component (FE-20)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:13:32 -06:00
Carlos Escalante
ad29cce6ac Skip 401 logout/redirect when already on the login page (FE-22)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:13:32 -06:00
Carlos Escalante
bebd7b563e Add security headers and proxy error handling to Hono server
Every response gets X-Frame-Options, X-Content-Type-Options, and
Referrer-Policy (SEC-07/FE-08). Backend proxy failures now return a
logged 502 instead of an opaque uncaught error (FE-23).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:13:32 -06:00
Carlos Escalante
3a357e4f0b Cap paste-import payload at 1 MB (ARCH-05)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:13:32 -06:00
Carlos Escalante
b4123703ef Rate-limit logins, constant-time creds, pin CORS, secure cookie
Both login endpoints now share verify_admin_credentials (hmac
compare_digest, SEC-02) and a 5/min/IP sliding-window rate limit that
returns 429 with Retry-After (SEC-03). CORS is pinned to the prod
domain and localhost dev origins instead of '*' (SEC-04). The session
cookie's Secure flag is driven by COOKIE_SECURE instead of hardcoded
false (SEC-08).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:13:19 -06:00
Carlos Escalante
15ea1ef4c2 Fail fast on missing or weak auth secrets; 7-day tokens
SECRET_KEY, ADMIN_USERNAME, and ADMIN_PASSWORD no longer have working
defaults: the backend refuses to boot if they are unset, default, or
weak (SEC-01). Token expiry drops from 30 to 7 days (SEC-05). New
COOKIE_SECURE (default true) and CORS_ORIGINS settings. Dev compose now
sources credentials from .env with required-var errors instead of
hardcoding them (ARCH-14); .env.example documents the template.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 19:13:19 -06:00
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
Carlos Escalante
20b4ad102d Wrap transaction_type in col() for notin_ filter
All checks were successful
Deploy to VPS / deploy (push) Successful in 12s
SQLModel enum columns need col() to expose SQLAlchemy operators like
notin_. Without it the agent tool raised at query build time and the
chat card flashed away.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:58:28 -06:00
Carlos Escalante
ec716e698f Exclude SALARY and DEPOSITO from agent recent-transactions tool
All checks were successful
Deploy to VPS / deploy (push) Successful in 13s
The 'last N transactions' answer was including salary deposits, which the
user reads as expense activity. Filter income types out at the query level.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:55:53 -06:00
Carlos Escalante
f556c392fb Pass OPENAI_API_KEY and AGENT_MODEL to prod from Gitea secrets
All checks were successful
Deploy to VPS / deploy (push) Successful in 13s
Backend was hitting OpenAI with no key (401) because the deploy workflow
never wrote OPENAI_API_KEY into .env.prod. Add it plus AGENT_MODEL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:49:55 -06:00
Carlos Escalante
aa4bb6512f Proxy /api/v1 and /api/auth from Hono to FastAPI in prod
All checks were successful
Deploy to VPS / deploy (push) Successful in 45s
In production the browser talks to the Hono server, which only proxied
/api/copilotkit/*. All other /api/* requests hit the SPA static fallback
and got index.html back. Forward /api/v1/* and /api/auth/* to BACKEND_URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:43:34 -06:00
Carlos Escalante
6b3069eef4 Fix prod backend hostname collision on nginx-prod-network
All checks were successful
Deploy to VPS / deploy (push) Successful in 5s
Frontend joins both wealthysmart-network-prod and nginx-prod-network
(needed for nginx-proxy reverse proxy + TLS). Another container on
nginx-prod-network is named "backend" too (receipts-backend-prod),
so DNS resolved "backend" to a sibling app and the agent endpoint
returned 404. Pin the agent/backend URLs to the unique container
name wealthysmart-backend-prod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:31:16 -06:00
Carlos Escalante
ead8fb8684 Fix prod frontend container: tsx on PATH and cap build heap
All checks were successful
Deploy to VPS / deploy (push) Successful in 5s
Runner stage was invoking `tsx server.ts` via sh, which doesn't
have node_modules/.bin on PATH, so the container crash-looped with
"tsx: not found" (502 at the edge). Use the absolute binary path
instead.

Also caps the Vite build's V8 heap to 1.5 GB so a future build on
the 4-core / 8 GB VPS can't OOM-kill neighbouring services.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:21:45 -06:00
Carlos Escalante
097fe9c4cf Point sync-db at old-vps and add A2UI theming notes
All checks were successful
Deploy to VPS / deploy (push) Successful in 1m17s
The production SSH alias is old-vps; the placeholder "production"
alias does not exist. Also captures research findings on theming
the A2UI basic catalog without overriding component internals,
for later reference.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:02:58 -06:00
Carlos Escalante
c92bfc66fe Update pages and components for new module paths
Repoints imports at the relocated lib/api and src/contexts modules,
and refreshes Layout + Login alongside the rest of the migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:02:46 -06:00
Carlos Escalante
cf8b7be778 Fix Tabs orientation selectors
Tailwind variants like data-horizontal and group-data-horizontal
never match the data-orientation=horizontal attribute Base UI
emits, so the flex layout collapsed and TabsList stretched
vertically. Switch to data-[orientation=...] selectors that
actually fire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:02:33 -06:00
Carlos Escalante
8b3a19b552 Add Skeleton primitive and budget detail loading state
Replaces the blank flash on the budget detail tab with skeleton
placeholders that mirror the final card layout, so the page no
longer shifts when the API returns.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:02:22 -06:00
Carlos Escalante
5d5727ec4e Add Asistente chat page with A2UI render tools
Wires CopilotKit v2 chat into the SPA as the Asistente page,
declares a render_spending_summary action backed by a custom
SpendingSummaryCard, and configures static suggestions shown
before the first message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:02:12 -06:00
Carlos Escalante
140a75f706 Add cookie-based SPA auth and update container plumbing
Backend now exposes /api/auth/login + /api/auth/logout setting an
httpOnly ws_token cookie, and get_current_user accepts either the
cookie (SPA) or a Bearer token (n8n/CLI). AuthContext probes the
cookie via /api/v1/auth/me. Dockerfiles and compose files updated
for the new agent service deps and CopilotKit dev sidecar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:02:02 -06:00
Carlos Escalante
7f602a67af Add Microsoft Agent Framework assistant with read-only tools
Wires up an OpenAI-backed MAF agent that exposes WealthySmart
data through tool calls (recent transactions, cycle summary,
analytics, pensions). Pulls in agent-framework + AG-UI adapter
+ OpenAI client deps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:01:50 -06:00
Carlos Escalante
5f2a4105f3 Migrate frontend to Vite + Hono CopilotKit runtime
Replaces the Next.js scaffold with a Vite SPA paired with a Hono
sidecar that hosts the CopilotKit runtime and proxies AG-UI traffic
to the MAF backend. Adds dev/prod Dockerfile, .dockerignore,
.gitignore, pnpm workspace config, and updates entrypoints
(main.tsx / App.tsx / index.css / index.html) plus the service
worker accordingly.

Server middleware reconciles MAF MESSAGES_SNAPSHOT id mismatches
so post-tool-call assistant text doesn't render twice, suppresses
duplicate text emitted alongside render tools, and strips OpenAI
training-token leaks from streamed deltas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:01:40 -06:00
Carlos Escalante
c4768e6912 Drop legacy pages, contexts, and dashboard widgets
Removes Dashboard / Transactions / Transfers pages, the section
configuration UI, the legacy useSettings hook, and the standalone
PrivacyContext/ThemeContext modules. Privacy/theme contexts now live
under src/contexts/ and the API helper / push-notifications module
move under src/lib/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:01:26 -06:00
Carlos Escalante
9fe17c0607 Drop legacy Next.js + CRA scaffold assets
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 22:01:06 -06:00
Carlos Escalante
98d32df763 Ignore tech_docs and local Claude state
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 21:57:58 -06:00
Carlos Escalante
d4d0f65759 Exclude income transactions from budget transactions list
All checks were successful
Deploy to VPS / deploy (push) Successful in 16s
Salary/deposit transactions were showing in the "Efectivo y
Transferencias" tab on the Budget page with a negative sign,
which is confusing since that view is for expenses only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:27:50 -06:00
Carlos Escalante
d929ed6573 Remove Ahorro from budget UI, add SALARY type and savings auto-accrual
All checks were successful
Deploy to VPS / deploy (push) Successful in 23s
Ahorro was already deducted from gross salary so displaying it in
budget projections was misleading. This removes the Ahorro card,
summary line, Proyecciones column, and Ahorro Anual card from the UI,
and strips all savings fields from budget API responses.

Adds SALARY TransactionType so salary deposits can be distinguished
from generic DEPOSITO transfers. When a SALARY transaction arrives,
the system auto-increments MEMP and MPAT savings account balances
(+200K CRC each) once per month via an idempotent accrual log.

New CRUD endpoints at /api/v1/savings-accrual/ allow manual correction
of the accrual history. Feb+Mar 2026 are seeded as historical baseline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:13:29 -06:00
Carlos Escalante
94a8a894a6 Convert all currencies to CRC and poll rates every 6h
All checks were successful
Deploy to VPS / deploy (push) Successful in 14s
Budget/transactions/salarios totals summed Transaction.amount directly,
so USD/EUR entries were treated as CRC and effectively disappeared from
the dashboard (the analytics fix in 9a80f2a only covered analytics).
Adds a shared get_converted_amount_expr() helper driven by the full
Currency enum — USD/EUR via ExchangeRate-API, BTC/XMR via CoinGecko —
and wires it into every func.sum(Transaction.amount) site.

Also starts a background task in the FastAPI lifespan that force-refreshes
every currency 4x/day, persisting USD to the DB and updating in-memory
caches for the rest. Failures are swallowed per-currency so a CoinGecko
outage cannot take out USD/EUR, and the last-known rate is always retained.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 17:16:20 -06:00
Carlos Escalante
9a80f2a997 Convert USD and EUR to CRC in analytics endpoints
All checks were successful
Deploy to VPS / deploy (push) Successful in 13s
All three analytics endpoints (by-category, monthly-trend, daily-spending)
now convert foreign currency amounts to CRC using current exchange rates.
EUR/CRC rate derived from ExchangeRate-API (USD-based cross rate).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 20:41:38 -06:00
Carlos Escalante
efe6d88286 Add EUR currency support for international transactions
All checks were successful
Deploy to VPS / deploy (push) Successful in 50s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 20:29:37 -06:00
Carlos Escalante
4da00750a8 Fix migration to use IF NOT EXISTS and Postgres-compatible DEFAULT
All checks were successful
Deploy to VPS / deploy (push) Successful in 13s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:14:20 -06:00
Carlos Escalante
792cef5006 Fix analytics case() bug, add privacy mode, add prod DB sync script
All checks were successful
Deploy to VPS / deploy (push) Successful in 28s
Fix SQLAlchemy case() import in monthly-trend endpoint. Add
data-sensitive attributes to Analytics charts and tables for privacy
blur. Add scripts/sync-db.sh for one-click prod-to-local PostgreSQL
sync. Remove SQLite artifacts from gitignore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:10:58 -06:00
Carlos Escalante
78e20f30cb Replace axios with native fetch API wrapper
Drop axios dependency in favor of a lightweight fetch-based client
that preserves the same { data: T } interface, keeping all 25
consumer files unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:10:48 -06:00
Carlos Escalante
51c106dc6c Add Proyecciones page with yearly financial projections view
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:10:37 -06:00
Carlos Escalante
0fdb5447b7 Add deferred transactions, revamp budget projections and UI
Adds deferred_to_next_cycle flag to transactions for billing cycle
bleed-over handling. Overhauls budget projection engine and refreshes
Budget page with improved monthly detail and transaction columns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 20:10:23 -06:00
Carlos Escalante
37e04273b9 Add clickable legend toggles and charge trend chart
All checks were successful
Deploy to VPS / deploy (push) Successful in 14s
- Both water consumption and charge trend charts now have clickable legends
  to show/hide individual series
- Hidden series appear dimmed in the legend
- Added line chart showing charge evolution over time (one line per charge type)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:38:30 -06:00
Carlos Escalante
c005956458 Support multi-file upload for municipal receipts
All checks were successful
Deploy to VPS / deploy (push) Successful in 15s
Upload panel now accepts multiple PDFs at once (drag-drop or file picker),
shows a file queue with individual remove buttons, and displays per-file
results after processing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:23:14 -06:00