- scan: rows go down on a clock as well as on a full batch. The reader's folder
of 26 RAWs is under one batch of 50, so nothing of it reached the screen until
the reading was over — a wall that stood empty for the whole scan and then
filled in one go. Frames now reach the screen every 400ms while the roll is
still being read, so the count climbs and the first tiles are drawn as the
frames land.
- tiles: a HEIC or a RAW has its tile written the first time it is drawn, so
every later visit reads the tile back instead of the file.
- resume: a reading a reload cut off is offered by the next visit with a chip
instead of being run behind the reader's back.
- offline listing: the desk's listing is kept whole in IndexedDB under
`recipescam-offline`/`reads`, past the 200 000 characters localStorage held,
so an offline visit opens on the listing it kept instead of on nothing.
- offline writes: a vote or a view cast with no server is held and goes out when
one answers again, with a bar that says how many are waiting.
- sw: the shell precaches its own bundle, stylesheet and icon, read out of the
document — a worker that precached only the document answered an offline
launch with a page whose JavaScript the machine did not have.
- checks in docker/frontend/scripts: tree-scan-check (counts while reading,
resume after a reload, tiles of a long roll), offline-read-check,
offline-queue-check.
The request was a mask's EXPOSURE losing hue, and the mask pass has not done that
since 6d60d45: measured today through a page the service worker controls, with an
ellipse over the whole frame and +1 EV, the pixels inside move exactly as the
frame's own knob moves them — identical to the byte (mask+1 vs frame+1 over four
codes: 619 pixels of 1,709,450, all of them on the rim), and the hue each one
leaves behind is the same distribution to a hundredth of a degree over the
370,606 pixels that carry a hue in both reads (mean 2.07°, p99 15.31° for the mask
against 15.32° for the frame; on the vivid pixels, mean 0.83° at +1 EV). The one
place that still says otherwise is a doc on the Android branch, whose §3.2 quotes
the old line.
But the symptom is real, and the build that produces it is the one from before
that commit, where maskAdjust multiplied the three channels by the stop:
c = c * half(pow(2.0, a.x));
Three channels clip by three different amounts, so the differences between them
stop being scaled together and the hue goes with them. That bundle could still be
what a visitor runs, because of two files the deploy never took away:
- index.html was the only document the server handed over with no Cache-Control
at all (the .mjs, /assets, /wasm and /models locations all name their policy,
sw.js and the manifest both opted out). With no header the browser is free to
guess a freshness window out of Last-Modified — a tenth of the file's age — and
answer a navigation from its own cache for hours after a deploy. The page it
answers with names the previous build's hashed bundle, so the previous shader
is what runs, and a hard reload is the only way out. The SPA fallback lands on
the same file (an internal redirect re-matches locations), so /app and /library
were covered by the same guess.
- sw.js is the second place the pin lived. Its navigations are network-first, but
a plain fetch is not the network: it can be answered by the browser's cache, so
the network never came first — and the old shell's hashed bundle, once fetched,
is a STATIC path that the cache-first rule serves forever.
So: nginx names the policy for the shell, with the isolation pair restated
because an add_header in a location drops every inherited one and index.html is
the document that needs them — the wasm renderer's SharedArrayBuffer is behind
that pair. The worker reads its navigations past the browser's cache, precaches
the shell the same way (a cache.add of '/' consults that cache like any other
fetch, so a shell read inside a stale window would be stored as the offline shell
of the build that replaced it), and VERSION goes to v2, whose activate drops the
cache the old worker pinned — the old shell and the old bundle with it.
The root fix is still 6d60d45: this commit is what lets it reach the browser.
Verified: nginx -t on the shipped config, and a container of this config against
a copy of index.html hands / and /app `Cache-Control: no-cache` with all five
original headers intact, while /assets/index-abc.js keeps `public, immutable`
(30 days) — the exact location does not shadow the hashed bundle. node --check on
sw.js. The measurements above come from the live 8090 build inside a persistent
profile whose page is controlled by the worker (controlled true,
crossOriginIsolated true, bundle index-CKOd57MG.js), the same session that pinned
the build the fix is about.
Co-authored-by: PenguinHarness <noreply@penguin.local>
The three things a browser asks for, without a plugin: a manifest in
public/ (name, /app as the start, three icons cut from the one piece of
art this repo has), a service worker, and the two metas iOS reads
instead of the manifest.
The worker caches the shell — /, /app, /library, all one document under
the SPA fallback — and the hashed assets the build emits. A navigation
is network-first, so a deploy is never pinned behind the cache; a
hashed asset or the wasm is cache-first, because under a given build
those never change. /api and any non-GET go straight out: a worker is a
cache, not a proxy. nginx serves sw.js and manifest.json `no-cache`
(both names outlive their contents) with the isolation headers the
worker script needs under COEP.
The offer is the app's own dialog, not Chromium's mini-infobar: the
event is held, and it is spent either after the visitor has been in the
studio two minutes or the moment an export lands — the point at which
the app has done their work. Safari never fires the event, so it gets
the Share > Add to Home Screen line instead. A refusal is remembered and
never asked again.
node scripts/make-icons.mjs 192x192 39785B / 512x512 159296B / maskable 512x512 123723B
node scripts/pwa-check.mjs manifest 3 icons · worker activated · shell cached
· offline reload of /app paints
off (https://localhost:8090) same four, through nginx