Upload statements from the web app

The Accounts screen gets an Add statements panel: pick an account, drop
files on it or choose them, and they are saved into that account's folder
and imported. The folder stays the source of truth -- an upload only puts
a file where `money import` looks, then runs the same import as the
Import button, so deleting index.db and re-importing still loses nothing.

Files travel base64 inside JSON rather than as multipart. There is no
auth, and the JSON-only rule is what keeps another site's form from
posting here; multipart is exactly what such a form can send.

An upload never replaces a statement: identical contents are a no-op and
different ones are refused with 409. Names import would not read back --
not a plain file name, dotfiles, account.toml, outside the account's
include patterns -- are refused, and a batch is checked whole before any
of it is written. Files are written through a dotfile and renamed, so a
concurrent import never reads half of one.

The overview now lists the account folders on disk, read fresh so one
created after startup is a valid target; it replaces the configured
count the empty accounts screen used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 18:51:56 +02:00
co-authored by Claude Opus 5.5
parent ec8a844441
commit 7ec9976b80
6 changed files with 489 additions and 22 deletions
+11
View File
@@ -237,6 +237,17 @@ otherwise name a different rule. Covered by `TestStalePositionIsRefused`.
There is no auth by design (`--addr` defaults to loopback). Non-GET requests
must be `application/json`, which is what keeps a cross-site form from posting
to it; do not relax that without putting something else in its place.
That is why `/api/upload` takes files base64 inside JSON rather than as
multipart: multipart is precisely what a cross-site form can send.
**An upload only puts a file where `money import` looks.** It writes into an
existing account folder and then runs the same import as `/api/import`, so the
statements stay the source of truth and nothing reaches the index any other
way. It never overwrites a statement (identical contents are a no-op, different
ones a 409), refuses any name import would not read back — not a plain file
name, a dotfile, `account.toml`, outside the account's `include` — and checks a
whole batch before writing any of it. Covered by
`TestUploadNeverReplacesAStatement` and `TestUploadRefusesBadNames`.
## Adding a bank parser