Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
8.0 KiB
Split gandora's index.html into ES modules — brief for a fresh agent
Superseded. The split already exists on the
splitbranch (seedocs/v2/NOTES-FROM-POC.md). Kept for its rules and checklist, which still describe how to review that branch before it merges.
You are refactoring gandora/site on teahub.xyz (Gitea; use the Teahub MCP tools: tea_file_get, tea_file_edit, tea_file_change, tea_commit, tea_branch_create, tea_pull_create; there is no git access from the container). The Mac hb-5 is reachable through boring_mac_exec (120 s per call; long jobs with nohup … & then poll); a clone lives at ~/scratch/gsite (git pull -q to refresh). Node is at /opt/homebrew/bin/node.
Work on the dev branch only. Do not push to main. Open one PR at the end.
Goal
index.html is a ~2,100-line single file holding the player, the DJ, rendering, covers, storage, the UI sheets and the usage ledger in one scope. Split it into ES modules with no build step: plain <script type="module">, static files served as-is by Pages, view-source still works, the service worker still caches a fixed list. Behaviour must be unchanged. The point is blast radius: after this, the player is the only file that needs the dev-branch-and-iPhone treatment.
Read first, in this order
HANDOFF.md— especially "Player rules" (iOS invariants learned the hard way) and "What a refactor should keep".ARCHITECTURE.md— the state machine and playback invariants.STORAGE.md— thedbadapter contract (chai's adapter targets it; keep the shape).tools/harness.sh,tools/harness.html,tools/harness-resume.html,tools/harness-cdp.py,tools/harness-safari.py— these are your tests.index.html,brain.js,sw.js,stats.html,webamp.html(the last two read IndexedDB and localStorage directly; don't change their contracts).
Target layout
index.html markup + CSS only, plus <script type="module" src="app.js"> and <script src="brain.js">
app.js boot: wires modules, sets window.g, hooks for brain.js, URL/share handling, SW + update pill, theme
player.js the state machine and the ONE <audio>: state, setSource, toCooking, toHolding, queueBehindHold,
handOver, startNow, playNow, toPlaying, toEnded, toIdle, playJob, advance, prime, bed/crackle/bedStart,
makeCrackle, SILENCE, Media Session, vizBeat/analyser
dj.js djPrompt, djText, askDJ, askBreak, askMash, DJ_SCHEMA, BREAKS/BREAK_BRIEF, MODES/MODE_BRIEF,
PERSONAS, VOICES/voiceParts/voiceLabel, speak, remixText, stationFrom helpers that only build specs
render.js venice() (as an injectable fetcher), renderTrack, hedge/queue/poll path, makeNoise, noiseSpec,
makeTone, toneSpec, wavBlob, wavBlob2, cost bookkeeping
covers.js pure: hash32, prng, keyHue/palettes, drawCover, drawRings, drawChord, attractor, drawMark,
drawOnAir, tape covers. No DOM lookups, no state — take a canvas and a spec.
store.js db (open/get/put/all/del), stations (saveStation, listStations data), tapes incl. the KEPT virtual
tape, tapeSpec, logEvent (the ledger), LS keys
ui/sheet.js stationSheet (name, look, palette, mode, dj details, sliders, actions)
ui/library.js library view, in-place play, now-playing bar, 🦙 link
ui/player.js cover/title/tags/lyrics rendering, keep/share buttons, scrubbing, drawTicks, markReaction, emoji
tray + picker, react(), toasts, tap-to-play veil
mashup.js mashup() (OfflineAudioContext stitch) and askMash wiring
brain.js unchanged (it is a classic script; keep its globals and the hooks contract)
Names are suggestions; the boundaries are not. Hard boundaries:
- Only
player.jstouchesstate.audio(or anyAudioelement that carries the station). Everything else calls its exports or listens to its events. covers.jsand the generators inrender.jsare pure (nodocument, nostate), so they can be checked in node.brain.jskeeps working as-is: it readshooks,state,cssv(),db; expose exactly what it needs onwindowfromapp.js(see below).- The
venicefetcher must be injectable (e.g.render.setFetcher(fn)), because the harness fragments replace it. Reassigning a module binding from outside won't work.
Globals for the harness and brain.js
The harness fragments and brain.js reach into state, crackle, openStation, cookNext, venice, db, hooks, wavBlob, $. In app.js, after wiring, set
window.g = { state, player, dj, render, store, covers, openStation, cookNext, crackle, hooks, db, wavBlob, $ };
and additionally put on window the exact names brain.js uses today (grep it). Then update the three harness fragments to read through window.g (minimal edits; same assertions). tools/harness-cdp.py and tools/harness-safari.py already serve over http; modules need http, so the file:// loop mode of harness.sh is retired: make sh tools/harness.sh run the loop fragment through the CDP path (it already does for resume).
Service worker
sw.js precaches a fixed FILES list and stamps REV from the commit. Add every new module to FILES. Make sure a half-updated cache can't mix versions: the update pill / RELOAD path must refresh all of them together (one cache name keyed by REV is enough; check it already is).
CSP
index.html carries a strict CSP (script-src 'self' 'unsafe-inline'). Module scripts from 'self' are fine; don't add inline module code and don't add any CDN.
Method — in order of distance from the audio element
Do it as a sequence of small commits on dev, each leaving the page working and the harnesses green:
covers.js+ the generators (pure) → node check:node -eimport them and draw/generate once.store.js(db, tapes, ledger).stats.htmlandwebamp.htmlmust still read the same IndexedDB/localStorage shapes.dj.jsandrender.jswith the injectable fetcher.ui/*.player.jslast and alone, in its own commit, touching nothing else. Re-read HANDOFF "Player rules" before this step. Do not "improve" the state machine while moving it.app.jswiring,window.g, harness fragment edits,sw.jslist,HANDOFF.md/ARCHITECTURE.mdupdated to the new layout (short; keep the invariants section intact and add "file map").
After each step run, on hb-5 in ~/scratch/gsite (after git pull -q):
/opt/homebrew/bin/node --check app.js player.js dj.js render.js covers.js store.js ui/*.js # syntax
sh tools/harness.sh # loop, Chrome over CDP
sh tools/harness.sh resume # resume with autoplay blocked, expects TAP TO PLAY
sh tools/harness.sh safari # WebKit (iPhone's engine) with a real activation
All three must print the same expectations as before the split (compare against a run on main first and save that output). A diff in any expect … line is a regression; fix or revert the step.
Rules
- No behaviour changes. No TypeScript, no bundler, no framework, no new dependencies, no reformatting of lines you aren't moving (keep diffs reviewable).
- Preserve every comment that explains an invariant; move it with the code.
- Keep
index.htmlthe entry URL; keep the share link formats (/#seed,/?t=), the IndexedDB name/stores/record shapes, and alllocalStoragekeys exactly. - Don't touch
brain.js,webamp.html,stats.html,tools/icons.py,.gitea/workflows/shots.yml,manifest.json, icons. - If something can't be split without a behaviour change, leave it in place and say so in the PR rather than changing behaviour.
- Commit messages: one line saying what moved, with the attribution lines in use on this repo.
Deliver
A PR dev → main with: the file map; the harness outputs before and after (all three modes); a list of every window global exposed and who uses it; anything you left unsplit and why; and the exact sw.js FILES list. Stop there — a human taps through dev.gandora.lol on an iPhone before merge.