voice.js was missing from build-dist.sh, which would have shipped a Worker
whose index.js imports a 404 - the app wouldn't have loaded at all. That's the
failure mode of an allowlist, so check it: any top-level .js not in MODULES now
fails the build instead of silently vanishing from the deploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK7PKpRVoy69Fpa32R8abb
A 🔊 toggle on the home screen counts you into each frame - "five, four,
three, two, one" - plus start and finish announcements. Useful when you're in
front of the camera rather than at the laptop. Uses the Web Speech API, so
it's local and needs no configuration.
The narrator runs off the intervalometer's state updates rather than a timer
of its own, so it can't drift away from what the sequence is doing; each
second is spoken at most once even though state arrives ~5x/sec.
The count is capped at one second under the interval, so a 3s interval says
"two, one" instead of talking over the previous frame, and a 1s interval stays
silent. A start delay isn't clamped, since that gap is whatever you set.
Countdown length, voice choice and frame-number announcements live in the
settings drawer; only the toggle is on the home screen.
Also:
- The toggle sits in the status card rather than the button row: three buttons
don't fit a 400px column, and it wrapped onto its own flex line where it
stretched to the wrong height.
- tsconfig excludes dist/, which build-dist.sh fills with copies of these same
files and which otherwise gets type-checked twice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK7PKpRVoy69Fpa32R8abb
Replaces the gPhoto2 demo UI (live view beside the raw config tree) with a
timelapse intervalometer. The home screen carries only what changes between
runs - interval, frame count, start delay, start/stop, countdown, progress
and a log - while every camera setting moves into a settings drawer.
- intervalometer.js: schedules frames on an absolute grid (start + n *
interval) so transfer time doesn't accumulate as drift over a long run. An
overrun logs and fires as soon as the camera is free rather than dropping a
frame; three consecutive failures abort.
- storage.js: frames stream into a folder via the File System Access API,
named for capture time (20260801-172713_00001.JPG) so sorting by name is
sorting by time. Falls back to downloads. Also holds the screen wake lock,
since background tabs get their timers throttled.
- config-utils.js: config tree lookups, shutter speed parsing, and bulb
capability detection (bulb toggle or Canon eosremoterelease).
- home.js: sequence controls plus a read-only exposure readout and pre-flight
warnings when the interval can't fit the exposure and transfer.
- settings.js / index.js: app prefs and the full config tree behind a drawer.
Config polling now only runs while that drawer is open, leaving the USB link
to the captures during a sequence.
Live view stays up between frames and steps aside only while the shutter
fires, which is what the EOS driver requires; it recovers afterwards with
backoff instead of hammering a busy camera.
Deployed as an assets-only Cloudflare Worker. The WASM is built with pthreads
and allocates a shared WebAssembly.Memory, so _headers reproduces the COOP/COEP
pair from serve.json - without cross-origin isolation the app fails to start.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK7PKpRVoy69Fpa32R8abb