Two holes left by the last change. A frame is opened with `window.location.href`
rather than a link, so the studio was still handed a fresh page and the scan with
it — every route out of the catalogue now goes through `go()`, which pushes the
address instead of reloading while a scan is in flight. And a row counted what was
filed away rather than what had been read, so it sat at zero for the whole of a
first scan: the catalogue's frames only reach IndexedDB in one batch at the end.
`ScanProgress.counts` carries the frames the scan has reached, per row, off the
scan's own bookkeeping, and the column reads it while the scan runs — the row
under the reader's eye moves as the roll is read, and the toolbar's line stays the
whole picture. A re-read counts from the start, which is what it is doing.
The regression check grows the two: a frame opened mid-scan from the catalogue
(of a roll it already holds) has to stay in the page, and the row has to count.
The catalogue hands the visitor to the studio with a plain `<a href>`, and the
app has no router: following it threw the page away, so a scan that was halfway
through a roll died with it. A scan belongs to the tab, not to the screen that
started it.
The scan now lives outside the component (`scanSession`/`startScan`/`stopScan`,
watched by whoever is up), so the catalogue can unmount and come back to a
reading that never stopped; a screen that returns joins it and sees the toolbar
line, the stop button and the rows filling in. The internal links are taken over
for a history push *only while a scan is in flight* — everywhere else the
browser navigates exactly as before, so the landing-to-studio flow is untouched.
`scripts/scan-nav-check.mjs` is the regression: a roll read one frame at a time
off a slow server, handed to the studio mid-scan, back to the catalogue, and the
whole roll read to the end with the studio up, counting documents along the way.
A folder's own menu gains a rename, which paints a label over the folder
without moving it — the frame ids and the recipes hang off the directory's
name, not the label. The empty part of the column answers a right click
with ADD FOLDER, the FOLDERS heading goes (the count already rides on
each row), and the column narrows to 118px so the stage takes the room.
The library page was a flat wall of thumbnails. It now reads like the admin
pictures tab: a folder tree on the left, the picked node's frame on the stage,
that node's frames in the strip below, and a chip pair to swap the stage for a
wall of every thumbnail in the node.
Folders are walked recursively (dot/@-prefixed names skipped, an unreadable
subfolder is dropped rather than failing the scan), so a roll with a dated
subfolder keeps both levels. Node counts include the subfolders underneath.
A RAW studio that cannot see a folder is one photo at a time. /library
now takes a folder through Chromium's directory picker, keeps the handle
in IndexedDB so the folder is there on the next visit, and walks it into
a grid: one thumbnail per frame, the frame's own date, and the recipe it
was last graded with. Nothing is uploaded and nothing is read twice —
the RAW itself is opened only when a tile is clicked, at which point the
studio develops it and the recipe comes back on top. The studio files
every change back against the frame, debounced, so reopening a RAW is
not doing the grade again.
Thumbnails come off LibRaw's unpack_thumb for a RAW, which is a seek and
a copy where a develop is a full decode of every pixel, and off
createImageBitmap for anything else. A RAW with no preview inside it
gets a placeholder tile rather than a minute of decoding per file.
node scripts/library-check.mjs
ok both frames indexed as tiles — 2 tiles from P1010256.JPG + P1010256.RW2
ok thumbnail for P1010256.JPG — 40400 bytes, jpeg=true
ok thumbnail for P1010256.RW2 — 39895 bytes, jpeg=true (LibRaw preview)
ok studio developed the frame from its handle
ok the address was handed back — url=/app (no ?lib= left behind)
ok the look was filed back against the frame — baseFilter=none, 19 knobs
ok the tile says the frame is edited