Files
site/HANDOFF.md
T

6.1 KiB
Raw Blame History

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).

Player rules (each one cost a regression on the phone; don't relearn them)

  • Player changes go to dev first (dev.gandora.lol), get tapped through on an iPhone, then merge to main. Anything touching startNow / toHolding / toPlaying / bed / crackleOn / prime / setSource counts. Features that don't touch the player (modes, DJ sheet, stats, covers) may go straight to main.
  • Run sh tools/harness.sh safari as well as the Chrome modes: WebKit is the iPhone's engine (play() policy, one audio session) and the Chrome loop mode currently hangs on hb-5.
  • iOS ignores audio.volume. A level is baked into the sample (makeCrackle(seconds, gain)), never set on the element.
  • Never set state.playing = true before play() resolves. The flag follows the promise (playNow); a refusal shows TAP TO PLAY. Setting it early gives a "playing" UI with no sound and a play button that needs two taps.
  • Only one <audio> carries the station. A second element (previews) can interrupt the first on iOS; after one plays, expect the next play() on the main element to be refused and handled by TAP TO PLAY.
  • The hold between tracks must keep ended firing: hold on a real looping clip with its own URL (silence when room tone is OFF), never on SILENCE, which the ended handler ignores.
  • The crackle bed is Web Audio and must never start over the music: bedStart refuses while the phase is playing and the element is playing or a play() is pending.

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.