No upload UI: the browser cannot put a photograph into the archive #142
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: MEDIUM
Split out of #101, which bundled it with logout and manual evidence as a third bullet of "missing
chrome". It is not chrome. It is the browser's first write path into the archive — the one place
where a person hands the software a file it will keep — and it deserves its own design rather than
being finished off at the end of a ticket about a sign-out button.
The problem
frontend/src/api/client.ts:92definesapi.uploadfor multipart form data. It has no callersanywhere in
frontend/src.POST /api/ingestexists and is tested, so the only way to put aphotograph into the archive today is
curl.Why it needs care
Ingest is not a form submission that either works or doesn't:
mid-upload, a file that fails validation on the ninetieth of a hundred — each needs an answer
that is not "start again".
IngestOutreports accepted and rejectedseparately. A UI that collapses that into one green tick tells the reviewer their scans are in
when some are not.
services/image_validation.py,services/image_sandbox.py), which refuse files deliberately. A refusal has to be legible —"this is not an image we can read" rather than a 400.
progress reads as a hang.
currently only happen for files that arrive by other means.
Open question, worth deciding before building
Whether browser upload is the intended ingest path at all, or whether a watch-directory worker
is. A scanner writes to a folder; a worker picks it up. If that is the real plan, then the browser
upload is a convenience for one-off additions rather than the main road, and its design should say
so — and the README should stop implying
curlis the only option either way.Done when
References
frontend/src/api/client.ts:92(api.upload, no callers)backend/app/api/routes/ingest.py;backend/app/services/ingest.pyDone in
50c3eb6.The open question, answered — and the constraint that answered it
@rbrooks: "this is not going to be running on a server that somebody scanning will have access to… the browser upload should be primary… the people scanning and uploading will be older and will get very frustrated with the tool if it's not very straightforward."
That settles it, and it settles it differently from how the issue framed the choice. The watch directory is not the alternative to browser upload — it is unreachable for the same reason the CLI is. The CLI folder scan #105 shipped stays for one-off use by someone with shell access; the browser is the road.
Measured before designing around it, because the issue left open whether browser upload was viable at all. A realistic 4×6 print at 400dpi (2.8 MB JPEG) costs 403 ms server-side through
POST /api/ingest— hashing, sandboxed validation, thumbnail and review derivatives, evidence, and the OCR enqueue (#144) included. Median of twelve, warm.So a box is a few minutes behind a progress bar, and no batch endpoint was built: one request per file gives per-file outcomes and resumability for free. The "API and maybe some easy-to-use remote tool" fallback is not needed.
Done when
already_ingestedIngest drops from admin to reviewer
A reviewer already dates photographs, records evidence, and can exclude a scan from the collection outright. Adding one is the smaller act, so admin-only read as a Phase 1 convenience rather than a considered boundary — and admin additionally carries role management and session revocation, which nobody needs in order to hand the archive a photograph.
The words, because they are the feature
#105's outcome vocabulary is written for a bulk-import log and never reaches the screen.
orphan_backbecomes "Added — this is the back of a photo, and its front isn't here yet", and it is filed under Needs a look rather than Added, because it is the only outcome the person can act on while the box is still open beside them. The mapping is a totalRecordover the generated union, so a new outcome is a compile error rather than anorphan_backin front of somebody's grandmother.Five things built that this issue did not ask for, each worth the space
DataTransfer.filesreports a dropped directory as one contentless entry — the naive version answers a dropped box of scans with a single "Skipped — not a photo file", which would have defeated the entire feature.readEntriesalso returns at most a hundred per call, the quiet way to lose everything past the hundredth photograph.aria-liveon "Adding 47 of 312" would announce two thousand times — the opposite of the intent. The visible bar carriesrole="progressbar"; the hidden region speaks about ten times a run.Files are sorted fronts before backs as
scan_folderdoes, or every back arrives first, becomes an orphan, and is folded and re-thumbnailed when its front turns up. Uploads run one at a time deliberately — the server holds SQLite's single write lock through each derivative, so parallelism would contend rather than help; that is commented so it is not later "optimised".A second hole in the safety net
Every case in
test_api_contract.pyasserts who is refused. So declaring a routeREVIEWERconstrained viewers and admins and said nothing about reviewers — reverting ingest to admin-only was caught by exactly one new test and nothing else.TestARoleFloorHolds.test_a_reviewer_is_not_locked_out_of_a_reviewer_routecloses it, mirroring the admin variant.Twelve mutations, all caught, including one that reverts the role and is killed by the end-to-end journey.
Not verified
readEntriesreturning an empty second batch, but Playwright cannot drop a real directory. That path is reviewed code, not exercised code.webkitdirectoryopening a real folder picker — the e2e journey uses the plain multi-file input.