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
4.7 KiB
4.7 KiB
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.htmlis ~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 globalstateobject 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.holdingis 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 inremember, the trail, shuffle, the library, the brain). Single rows go throughdb.get; tapes throughtapeById(KEPT is virtual). An index onspec.stationIdwould remove most of the remainingdb.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
playJobon an errored render (job.err) callscrackleOff()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) callscrackleOff()and returns withoutbed(); the room tone comes back on the next state change. Pre-existing.#skipduring a hold pauses the crackle-in-player; the phase staysholding(paused) until the landing track takes over throughtoPlaying. That is how it behaved before;bed()stays off meanwhile, as before.setSourcenow refuses to revoke the crackle element's object URL (the oldswapNowcould, on a rarely reachable branch). The only intended behaviour change; it can only prevent a dead crackle.toIdleresolves a parkedqueueBehindHoldpromise when the station changes, so thatplayJobreturns instead of leaking; before, it stayed pending forever. Harmless either way.prime()still setsstate.audio.srcitself 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
drawPaletteand the library tiles callplayJobdirectly with a resolved job; fine, but they bypassadvance(), so the session's back/forward list does not learn about them (as before). - The
#playbutton'stapStartedguard (ignore the click thatprime()already used to start a resume) is timing based (800 ms). Untouched. - STORAGE.md listed four
dbcalls and two stores; the code hasdb.getand atapesstore, 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 = 40unkept 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.