split index.html into ES modules (no build step); player.js is the only file that touches the audio element #3

Open
teahubxyz wants to merge 15 commits from split into main
pull from: split
Owner

Do not merge yet — a human taps through this branch on an iPhone first. (Branch note: the brief said dev, but PR #2 main→dev is stuck in Gitea's "mergeable: false / try again later" state, so split was branched from main instead — same content as a synced dev, main untouched. Point dev.gandora.lol at it, or fast-forward dev to it.)

Behaviour unchanged; every line moved, not rewritten (checked mechanically: every non-import line of every module occurs verbatim in main's index.html except the edits listed under "mechanical changes"). Plain <script type="module">, static files, view-source works, CSP unchanged, no dependencies.

File map

File Owns
index.html markup + CSS, <script type="module" src="app.js">, <script src="brain.js" defer>
core.js state (incl. the one <audio>), hooks, $, LS, toast, sleep, asDataUrl, mmss, the job counter ids
covers.js pure: hash32, prng, keyHue/KEYS, palettes, drawCover + the looks, drawMark, drawOnAir
player.js the state machine and everything touching state.audio: setSource, toCooking…toIdle, playJob, advance, step, prime, crackle/bed, SILENCE, scrubbing, Media Session, analyser, transport buttons — moved as-is (the only edits in it: two ++seq → ++ids.seq)
render.js venice() behind an injectable fetcher (setFetcher), renderTrack, makeNoise/noiseSpec, makeTone/toneSpec, wavBlob
dj.js djPrompt/askDJ, askBreak, askMash, schema, modes, breaks, voices, personas, speak
store.js db (STORAGE.md contract unchanged), saveStation, remember, logEvent, tapes incl. virtual KEPT, tapeSpec
mashup.js mashReference, mashup
ui/player.js, ui/library.js, ui/sheet.js, ui/taste.js cover/title/lyrics/reactions/emoji; stations row + library + now-playing + tapes; station sheet; taste view
app.js boot: openStation, makeJob/cookNext, links/share, SW + update pill, theme, key box; sets window.g and brain's globals
brain.js, webamp.html, stats.html, manifest.json, icons untouched

Commits

  1. tooling (tools/split.js line map + split-post.py, check-pure.mjs, harness fragments as inline modules over window.g, harness.sh loop mode over http/CDP, a split.yml workflow that performs a step on the runner — delete tools/split*, .gitea/workflows/split.yml after merge if you like)
  2. split 1/4 covers.js + core.js · 3. split 2/4 store.js · 4. split 3/4 dj.js + render.js (+ a one-line fix commit) · 5. split 4/4 player.js + ui/* + app.js + mashup.js · 6. docs.

Steps 1–3 leave the remainder as an inline module in index.html, so each commit is a working page. Step 4 is player + ui + app together, not player alone as the brief asked: player ↔ ui/player ↔ app are one import cycle, and the first commit that extracts any of them must extract all of them. player.js itself is a pure cut, so the review of "did the state machine change" is diff of its lines against main's index.html (none).

Mechanical changes (everything that is not a pure move)

  • venice → veniceFetch + let fetcher, export const setFetcher, export const venice = (...a) => fetcher(...a) (render.js). Module bindings can't be reassigned from outside; the harness stubs through setFetcher.
  • let seq → export const ids = { seq: 0 } (core.js) and ++seq → ++ids.seq in 7 places (an imported let can't be incremented). brain.js keeps its ++seq through a window.seq accessor onto ids.seq: one counter.
  • makeTone/makeNoise exported (for the node check).
  • brain.js loads with defer so it runs after the module (hooks are assigned a few ms later than before, still before any interaction).
  • Modules are strict mode; scanned main's script for undeclared assignments: none.
  • sw.js FILES lists the modules (one cache name keyed by REV, addAll is atomic, old caches dropped on activate — a half-updated mix can't happen; REV is stamped by shots.yml on push to main as before).

window globals exposed (app.js), and who uses them

  • window.g = { state, openStation, cookNext, crackle, hooks, db, wavBlob, $, hash32, render: { setFetcher } } — the three harness fragments.
  • state, hooks, $, db, playJob, drawCover, hash32, prng, keyHue, KEYS, reactionWeight, drawTaste, openTaste, reactionEmoji and the seq accessor — exactly the names brain.js uses (grep'd); brain.js unchanged.

Left unsplit / not changed, and why

  • Residual state.audio touches outside player.js, left because moving them is a behaviour change or a rewrite: the station sheet's volume slider (state.audio.volume), ui/player.js reading currentTime/duration for ticks and reactions and seeking on a tick tap, playTape/step/the library tile pausing before playJob, brain.js pausing from the taste view. Listed in HANDOFF.
  • covers.js is "pure" in the sense of no state and no $: it still uses document.createElement("canvas") for its offscreen caches and getComputedStyle for the ON AIR colours, as before. tools/check-pure.mjs runs it in node under a 10-line DOM stub.
  • prof temp dir in harness.sh is now unused (harness-cdp makes its own); harmless.

Harness, before (main @ 0786964) and after (split @ 4aa94d8), on hb-5

All three modes byte-identical before/after, after every step (1, 2, 3, 4, tip). Baseline run twice: stable.

Note on the Chrome loop mode: --dump-dom --virtual-time-budget hangs on hb-5 (known, HANDOFF), so the baseline loop was taken through the same http+CDP path as resume. In real time its lines from "track landed" on differ from the fragment's expect text (a 2 s test wav really ends); that is main's behaviour under real time, reproduced exactly after the split. Safari matches every expect line.

# loop (Chrome, CDP, autoplay allowed) — identical before/after
after first          expect phase=playing cur=T1 playing, next=T2 nextblob=true
    phase=playing src=http://12 paused=false playing=true holding=false cur=T1 next=T2 nextblob=true crackle=false loop=false
after ended          expect cur=T2 playing, holding=false
    phase=playing src=http://12 paused=false playing=true holding=false cur=T2 next=T3 nextblob=true crackle=false loop=false
slow cook started    expect next cooking, nextblob=false
    phase=playing src=http://12 paused=false playing=true holding=false cur=T2 next=T4 nextblob=false crackle=false loop=false
ended, nothing ready expect phase=holding holding=true loop=true
    phase=holding src=http://12 paused=false playing=true holding=true cur=T4 next=null nextblob=false crackle=false loop=true
track landed         expect holding=true loop=false (queued behind the crackle)
    phase=playing src=http://12 paused=false playing=true holding=false cur=T4 next=T5 nextblob=false crackle=false loop=false
crackle pass ended   expect phase=playing cur=that track playing, holding=false
    phase=holding src=http://12 paused=false playing=true holding=true cur=T5 next=T6 nextblob=false crackle=false loop=true
after pause          expect paused=true crackle=true
    phase=holding src=http://12 paused=true playing=false holding=true cur=T5 next=T6 nextblob=false crackle=false loop=true
after resume         expect paused=false crackle=false
    phase=holding src=http://12 paused=false playing=true holding=true cur=T5 next=T6 nextblob=false crackle=false loop=true
after skip           expect a new cur
    phase=playing src=http://12 paused=false playing=true holding=false cur=T6 next=T7 nextblob=false crackle=false loop=false
END

# resume (Chrome, CDP, autoplay blocked) — identical before/after
before resume        expect phase=idle cur=null paused=true needTap=false
    phase=idle needTap=false tap=false veil="COOKING" paused=true playing=false holding=false cur=null curblob=false next=null nextblob=false crackle=false
resume, no gesture   expect phase=playing needTap=true tap=true veil="TAP TO PLAY" paused=true cur=R1 curblob=true next=T1 nextblob=true
    phase=playing needTap=true tap=true veil="TAP TO PLAY" paused=true playing=false holding=false cur=R1 curblob=true next=T1 nextblob=true crackle=false
(the tap that starts it needs a real user activation; not asserted here)
END

# safari (WebKit, safaridriver, real click) — identical before/after
after first          expect phase=playing cur=T1 playing, next=T2 nextblob=true
    phase=playing src=http://12 paused=false playing=true holding=false cur=T1 next=T2 nextblob=true crackle=false loop=false
after ended          expect cur=T2 playing, holding=false
    phase=playing src=http://12 paused=false playing=true holding=false cur=T2 next=T3 nextblob=true crackle=false loop=false
slow cook started    expect next cooking, nextblob=false
    phase=playing src=http://12 paused=false playing=true holding=false cur=T2 next=T4 nextblob=false crackle=false loop=false
ended, nothing ready expect phase=holding holding=true loop=true
    phase=holding src=http://12 paused=false playing=true holding=true cur=T4 next=null nextblob=false crackle=false loop=true
track landed         expect holding=true loop=false (queued behind the crackle)
    phase=holding src=http://12 paused=false playing=true holding=true cur=T4 next=null nextblob=false crackle=false loop=false
crackle pass ended   expect phase=playing cur=that track playing, holding=false
    phase=playing src=http://12 paused=false playing=true holding=false cur=T4 next=T5 nextblob=false crackle=false loop=false
after pause          expect paused=true crackle=true
    phase=playing src=http://12 paused=true playing=false holding=false cur=T4 next=T5 nextblob=false crackle=true loop=false
after resume         expect paused=false crackle=false
    phase=playing src=http://12 paused=false playing=true holding=false cur=T4 next=T5 nextblob=false crackle=false loop=false
after skip           expect a new cur
    phase=cooking src=http://12 paused=true playing=false holding=false cur=T5 next=T6 nextblob=true crackle=true loop=false
END

Also: node --check on every module at every step; node tools/check-pure.mjs (covers: every look + ON AIR; render: wav/noise/tone) green; page load over CDP with brain.js: no uncaught exceptions, hooks assigned, window.g present.

sw.js FILES

["/", "/index.html", "/app.js", "/core.js", "/covers.js", "/dj.js", "/mashup.js", "/player.js", "/render.js", "/store.js", "/ui/library.js", "/ui/player.js", "/ui/sheet.js", "/ui/taste.js", "/stats.html", "/favicon.svg", "/manifest.json", "/icons/icon.svg"]

🤖 Generated with Claude Code

https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG

**Do not merge yet** — a human taps through this branch on an iPhone first. (Branch note: the brief said `dev`, but PR #2 main→dev is stuck in Gitea's "mergeable: false / try again later" state, so `split` was branched from `main` instead — same content as a synced dev, main untouched. Point dev.gandora.lol at it, or fast-forward dev to it.) Behaviour unchanged; every line moved, not rewritten (checked mechanically: every non-import line of every module occurs verbatim in main's index.html except the edits listed under "mechanical changes"). Plain `<script type="module">`, static files, view-source works, CSP unchanged, no dependencies. ## File map | File | Owns | |---|---| | `index.html` | markup + CSS, `<script type="module" src="app.js">`, `<script src="brain.js" defer>` | | `core.js` | `state` (incl. the one `<audio>`), `hooks`, `$`, `LS`, `toast`, `sleep`, `asDataUrl`, `mmss`, the job counter `ids` | | `covers.js` | pure: `hash32`, `prng`, `keyHue`/`KEYS`, palettes, `drawCover` + the looks, `drawMark`, `drawOnAir` | | `player.js` | the state machine and everything touching `state.audio`: `setSource`, `toCooking`…`toIdle`, `playJob`, `advance`, `step`, `prime`, crackle/bed, `SILENCE`, scrubbing, Media Session, analyser, transport buttons — **moved as-is** (the only edits in it: two `++seq` → `++ids.seq`) | | `render.js` | `venice()` behind an injectable fetcher (`setFetcher`), `renderTrack`, `makeNoise`/`noiseSpec`, `makeTone`/`toneSpec`, `wavBlob` | | `dj.js` | `djPrompt`/`askDJ`, `askBreak`, `askMash`, schema, modes, breaks, voices, personas, `speak` | | `store.js` | `db` (STORAGE.md contract unchanged), `saveStation`, `remember`, `logEvent`, tapes incl. virtual KEPT, `tapeSpec` | | `mashup.js` | `mashReference`, `mashup` | | `ui/player.js`, `ui/library.js`, `ui/sheet.js`, `ui/taste.js` | cover/title/lyrics/reactions/emoji; stations row + library + now-playing + tapes; station sheet; taste view | | `app.js` | boot: `openStation`, `makeJob`/`cookNext`, links/share, SW + update pill, theme, key box; sets `window.g` and brain's globals | | `brain.js`, `webamp.html`, `stats.html`, `manifest.json`, icons | untouched | ## Commits 1. tooling (`tools/split.js` line map + `split-post.py`, `check-pure.mjs`, harness fragments as inline modules over `window.g`, `harness.sh` loop mode over http/CDP, a `split.yml` workflow that performs a step on the runner — delete `tools/split*`, `.gitea/workflows/split.yml` after merge if you like) 2. `split 1/4` covers.js + core.js · 3. `split 2/4` store.js · 4. `split 3/4` dj.js + render.js (+ a one-line fix commit) · 5. `split 4/4` player.js + ui/* + app.js + mashup.js · 6. docs. Steps 1–3 leave the remainder as an inline module in index.html, so each commit is a working page. **Step 4 is player + ui + app together**, not player alone as the brief asked: `player` ↔ `ui/player` ↔ `app` are one import cycle, and the first commit that extracts any of them must extract all of them. player.js itself is a pure cut, so the review of "did the state machine change" is `diff` of its lines against main's index.html (none). ## Mechanical changes (everything that is not a pure move) - `venice` → `veniceFetch` + `let fetcher`, `export const setFetcher`, `export const venice = (...a) => fetcher(...a)` (render.js). Module bindings can't be reassigned from outside; the harness stubs through `setFetcher`. - `let seq` → `export const ids = { seq: 0 }` (core.js) and `++seq` → `++ids.seq` in 7 places (an imported `let` can't be incremented). brain.js keeps its `++seq` through a `window.seq` accessor onto `ids.seq`: one counter. - `makeTone`/`makeNoise` exported (for the node check). - `brain.js` loads with `defer` so it runs after the module (hooks are assigned a few ms later than before, still before any interaction). - Modules are strict mode; scanned main's script for undeclared assignments: none. - `sw.js` FILES lists the modules (one cache name keyed by REV, `addAll` is atomic, old caches dropped on activate — a half-updated mix can't happen; REV is stamped by shots.yml on push to main as before). ## `window` globals exposed (app.js), and who uses them - `window.g = { state, openStation, cookNext, crackle, hooks, db, wavBlob, $, hash32, render: { setFetcher } }` — the three harness fragments. - `state, hooks, $, db, playJob, drawCover, hash32, prng, keyHue, KEYS, reactionWeight, drawTaste, openTaste, reactionEmoji` and the `seq` accessor — exactly the names brain.js uses (grep'd); brain.js unchanged. ## Left unsplit / not changed, and why - Residual `state.audio` touches outside player.js, left because moving them is a behaviour change or a rewrite: the station sheet's volume slider (`state.audio.volume`), `ui/player.js` reading `currentTime`/`duration` for ticks and reactions and seeking on a tick tap, `playTape`/`step`/the library tile pausing before `playJob`, brain.js pausing from the taste view. Listed in HANDOFF. - `covers.js` is "pure" in the sense of no `state` and no `$`: it still uses `document.createElement("canvas")` for its offscreen caches and `getComputedStyle` for the ON AIR colours, as before. `tools/check-pure.mjs` runs it in node under a 10-line DOM stub. - `prof` temp dir in harness.sh is now unused (harness-cdp makes its own); harmless. ## Harness, before (main @ 0786964) and after (split @ 4aa94d8), on hb-5 All three modes **byte-identical** before/after, after every step (1, 2, 3, 4, tip). Baseline run twice: stable. Note on the Chrome loop mode: `--dump-dom --virtual-time-budget` hangs on hb-5 (known, HANDOFF), so the baseline loop was taken through the same http+CDP path as resume. In real time its lines from "track landed" on differ from the fragment's `expect` text (a 2 s test wav really ends); that is main's behaviour under real time, reproduced exactly after the split. Safari matches every `expect` line. ``` # loop (Chrome, CDP, autoplay allowed) — identical before/after after first expect phase=playing cur=T1 playing, next=T2 nextblob=true phase=playing src=http://12 paused=false playing=true holding=false cur=T1 next=T2 nextblob=true crackle=false loop=false after ended expect cur=T2 playing, holding=false phase=playing src=http://12 paused=false playing=true holding=false cur=T2 next=T3 nextblob=true crackle=false loop=false slow cook started expect next cooking, nextblob=false phase=playing src=http://12 paused=false playing=true holding=false cur=T2 next=T4 nextblob=false crackle=false loop=false ended, nothing ready expect phase=holding holding=true loop=true phase=holding src=http://12 paused=false playing=true holding=true cur=T4 next=null nextblob=false crackle=false loop=true track landed expect holding=true loop=false (queued behind the crackle) phase=playing src=http://12 paused=false playing=true holding=false cur=T4 next=T5 nextblob=false crackle=false loop=false crackle pass ended expect phase=playing cur=that track playing, holding=false phase=holding src=http://12 paused=false playing=true holding=true cur=T5 next=T6 nextblob=false crackle=false loop=true after pause expect paused=true crackle=true phase=holding src=http://12 paused=true playing=false holding=true cur=T5 next=T6 nextblob=false crackle=false loop=true after resume expect paused=false crackle=false phase=holding src=http://12 paused=false playing=true holding=true cur=T5 next=T6 nextblob=false crackle=false loop=true after skip expect a new cur phase=playing src=http://12 paused=false playing=true holding=false cur=T6 next=T7 nextblob=false crackle=false loop=false END # resume (Chrome, CDP, autoplay blocked) — identical before/after before resume expect phase=idle cur=null paused=true needTap=false phase=idle needTap=false tap=false veil="COOKING" paused=true playing=false holding=false cur=null curblob=false next=null nextblob=false crackle=false resume, no gesture expect phase=playing needTap=true tap=true veil="TAP TO PLAY" paused=true cur=R1 curblob=true next=T1 nextblob=true phase=playing needTap=true tap=true veil="TAP TO PLAY" paused=true playing=false holding=false cur=R1 curblob=true next=T1 nextblob=true crackle=false (the tap that starts it needs a real user activation; not asserted here) END # safari (WebKit, safaridriver, real click) — identical before/after after first expect phase=playing cur=T1 playing, next=T2 nextblob=true phase=playing src=http://12 paused=false playing=true holding=false cur=T1 next=T2 nextblob=true crackle=false loop=false after ended expect cur=T2 playing, holding=false phase=playing src=http://12 paused=false playing=true holding=false cur=T2 next=T3 nextblob=true crackle=false loop=false slow cook started expect next cooking, nextblob=false phase=playing src=http://12 paused=false playing=true holding=false cur=T2 next=T4 nextblob=false crackle=false loop=false ended, nothing ready expect phase=holding holding=true loop=true phase=holding src=http://12 paused=false playing=true holding=true cur=T4 next=null nextblob=false crackle=false loop=true track landed expect holding=true loop=false (queued behind the crackle) phase=holding src=http://12 paused=false playing=true holding=true cur=T4 next=null nextblob=false crackle=false loop=false crackle pass ended expect phase=playing cur=that track playing, holding=false phase=playing src=http://12 paused=false playing=true holding=false cur=T4 next=T5 nextblob=false crackle=false loop=false after pause expect paused=true crackle=true phase=playing src=http://12 paused=true playing=false holding=false cur=T4 next=T5 nextblob=false crackle=true loop=false after resume expect paused=false crackle=false phase=playing src=http://12 paused=false playing=true holding=false cur=T4 next=T5 nextblob=false crackle=false loop=false after skip expect a new cur phase=cooking src=http://12 paused=true playing=false holding=false cur=T5 next=T6 nextblob=true crackle=true loop=false END ``` Also: `node --check` on every module at every step; `node tools/check-pure.mjs` (covers: every look + ON AIR; render: wav/noise/tone) green; page load over CDP with brain.js: no uncaught exceptions, hooks assigned, `window.g` present. ## sw.js FILES ``` ["/", "/index.html", "/app.js", "/core.js", "/covers.js", "/dj.js", "/mashup.js", "/player.js", "/render.js", "/store.js", "/ui/library.js", "/ui/player.js", "/ui/sheet.js", "/ui/taste.js", "/stats.html", "/favicon.svg", "/manifest.json", "/icons/icon.svg"] ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
teahubxyz added 15 commits 2026-10-21 22:37:20 +00:00
The harness fragments now run as inline modules (after app.js) and stub the renderer through render.setFetcher; harness.sh's loop mode goes over http + DevTools like resume (file:// cannot load modules; --dump-dom also hangs on current Chrome). No page change yet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dhu3iHseRewQ2WpBSVLokG
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin split:split
git checkout split
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gandora/site#3