The dev Vite proxy rewrites the request Host to 127.0.0.1:8000, so the
backend's absolute signed file URLs pointed at a different origin than the
SPA (localhost:5173). Images tolerated it, but the auth'd fetch that reads
the staging blob for IQDB was blocked ('Cross-Origin Request Blocked') and
every similarity check died before reaching e621.
apiUrl() now keeps API-built absolute URLs on the page's origin whenever the
SPA is in same-origin mode (dev proxy, deploy nginx) and leaves them
absolute when an explicit backend URL is configured. All consumers use it:
staging previews and the bulk modal, library cards, optimizer (range sniff +
worker), IQDB card, delete page, similar page.
e621 identification now follows the documented 'App/version (developer)'
form: server-side requests send 'J621/<hash> (JakeBreath)' and the browser
_client gets the same string, with the hash baked into the frontend image
(GIT_HASH build arg; guarded at runtime so the dev server still works).
IQDB stalls: requests now time out after 20s (a hung fetch used to block the
serialized e621 queue forever), and all checks run through one serial drain
so repeated 'Check similarity' clicks can no longer start overlapping runs
that re-download the same staging blobs. Auth/rate-limit/timeout/network
failures stop the queue with the reason and a retry button instead of
grinding through the rest.
Verified live: staged file URL is same-origin through the proxy and fetches
200 through it.
A 200-file bulk move is one long server-side copy+index chain per file, so
the old single request sat on a spinner the whole time (and got uncomfortably
close to the 120s proxy timeout). The modal now resolves the selection in
chunks of 8:
- footer switches to a progress bar with 'n moved / done / total / percent'
while running, and the header explains that files are being indexed;
- the rating pills, selection actions, grid and close button are locked
while it runs so progress can't be lost by accident;
- failures are collected with their filenames, the modal stays open for a
summary, and 'Retry failed' re-selects only the files that are still
pending; a clean run still auto-closes with a toast.
Upload board:
- the tile grid no longer re-sorts itself as files finish (that reshuffled
the list under the cursor); it keeps insertion order, uses auto-fill tiles
of ~150px so they hold a readable size, scrolls inside a 60vh area and no
longer chains the page scroll (overscroll-contain);
- the files currently in flight are pinned in a small live strip above the
grid (name, percent, bar) so progress stays visible while the grid is
scrolled with hundreds of tiles.
Bulk rating: a 'bulk rate' button in the Pending & Unmatched header opens a
large modal with Safe/Questionable/Explicit pills, a tickable thumbnail grid
(Select all / Clear) and one confirm that moves every selected upload into
the library with that rating. Backed by POST /api/uploads/resolve-bulk/
(temp_ids + rating, own rows only): each staged file is resolved as a custom
entry (keeps its staged tags/notes), and already-completed or foreign ids are
reported per entry instead of failing the whole batch. Built for the
358-file backlog.
Tests: 4 bulk-resolve tests (resolution with the rating, input validation,
foreign ids untouched, mixed completed+pending) — full backend suite 53
green. Verified live end to end: staged a file, bulk-resolved it as 'q', saw
J-96 created with that rating, then removed the item, temp row and test
token.