Files
site/HANDOFF.md
T
teahubxyzandClaude Fable 5.1 1aeebac6f2 harness: a resume mode (autoplay blocked); docs for the state machine and hooks
tools/harness.sh resume seeds IndexedDB with a station and one rendered
track, resumes it with no user gesture and expects the TAP TO PLAY wait.
IndexedDB needs http, so the page is served from a temp folder; headless
Chrome's virtual time doesn't wait for IndexedDB, so this mode is driven in
real time over the DevTools protocol (tools/harness-cdp.py, stdlib only).
The loop harness now prints state.phase too.

ARCHITECTURE.md describes the machine as implemented; HANDOFF.md lists what
was found and left alone; STORAGE.md gains db.get and the tapes store (the
code already had both); README points at the new files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-21 06:01:48 +00:00

69 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Handoff notes
For whoever picks this up: what is solid, what is held together with care, and what a refactor should and should not
do. Read ARCHITECTURE.md first.
## Where the seams are
- `index.html` is ~1,900 lines in one script. It grew feature by feature in one long session. The sections are
commented and ordered sensibly, but state is one global `state` object and functions reach into each other freely.
- brain.js plugs into the page through `hooks` (`djPrompt`, `jobMade`, `stationOpened`, `reacted`); it no longer
overrides page functions by assignment. Anything else loaded after the page should do the same.
- Playback is one small state machine (`state.phase`: idle → cooking → holding → playing → ended) with named
transitions; see ARCHITECTURE.md › The player's state machine. `state.holding` is a read-only view of the phase.
- The crackle exists twice: as the side element (room tone) and as the player's own source (the hold). That is
deliberate (iOS needs the hold in the same element) but easy to misread.
- `db.all()` is still used where a filter is needed (a station's tracks, the prune in `remember`, the trail, shuffle,
the library, the brain). Single rows go through `db.get`; tapes through `tapeById` (KEPT is virtual). An index on
`spec.stationId` would remove most of the remaining `db.all()` calls.
- Several things are computed from the sheet in more than one place (section-at-time, mmss, kind chips).
## Found while refactoring playback, and deliberately left alone
- `playJob` on an errored render (`job.err`) calls `crackleOff()` and leaves the phase where it was; if the player was
holding, the crackle keeps looping in the player until skip. Pre-existing; the machine reproduces it.
- `advance()` with nothing to play (`!job`) calls `crackleOff()` and returns without `bed()`; the room tone comes back
on the next state change. Pre-existing.
- `#skip` during a hold pauses the crackle-in-player; the phase stays `holding` (paused) until the landing track takes
over through `toPlaying`. That is how it behaved before; `bed()` stays off meanwhile, as before.
- `setSource` now refuses to revoke the crackle element's object URL (the old `swapNow` could, on a rarely reachable
branch). The only intended behaviour change; it can only prevent a dead crackle.
- `toIdle` resolves a parked `queueBehindHold` promise when the station changes, so that `playJob` returns instead of
leaking; before, it stayed pending forever. Harmless either way.
- `prime()` still sets `state.audio.src` itself for the unlock clip and for the TAP TO PLAY start; it is the one source
write outside the machine, by design.
- brain.js's `drawPalette` and the library tiles call `playJob` directly with a resolved job; fine, but they bypass
`advance()`, so the session's back/forward list does not learn about them (as before).
- The `#play` button's `tapStarted` guard (ignore the click that `prime()` already used to start a resume) is timing
based (800 ms). Untouched.
- STORAGE.md listed four `db` calls and two stores; the code has `db.get` and a `tapes` store, so the doc was brought up
to the code (the contract itself is unchanged).
## Known rough edges
- iOS volume sliders are inert (platform); the welcome/ident may fall back to Venice if Runware times out on a
spoken-tag render (now avoided: spoken breaks are TTS).
- The mashup reference uses OfflineAudioContext; untested headlessly.
- The quota is per-person-per-day only; KV free tier caps total writes at ~1,000/day.
- Share links made before the render seed was included re-roll the song.
- `KEEP_UNKEPT = 40` unkept tracks roll off; nothing warns.
- Light theme: a few hard-coded colours may remain in rarely-seen sheets.
- The harness's resume mode checks the TAP TO PLAY wait but cannot press the tap: headless Chrome has no real user
activation. The tap half is a phone test.
## What a refactor should keep
- Every invariant in ARCHITECTURE.md › Playback invariants, and the harness green (both modes) before and after.
- The sheet as the unit (everything derives from `spec`), covers as a pure function of it.
- One static page, no build step, no framework, no keys in the page.
- STORAGE.md's contract (chai's DB adapter targets it).
## What's next (agreed directions, not started)
- Text embeddings for every sheet, audio embeddings (Gemini Embedding 2) for kept/reacted tracks; the taste map
laid out in embedding space; segment-level vectors with the timed reactions (`spec.hits`).
- Free tier = shared allowance on the worker (the key box is parked); pro = longer tracks, XL renders, sync.
- Room-tone picker (vinyl / rain / tape / brown / drone in key).
- Butterchurn (Milkdrop) as a fourth look.
- Taste file export/import.