docs/v2: brief for splitting index.html into ES modules (no build step)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
This commit is contained in:
teahubxyzandClaude Opus 5.5 committed 2026-10-22 06:14:24 +00:00
1 parent 7520b8e037
commit 71da0c9a81
1 file changed
+101
+101
View File
@@ -0,0 +1,101 @@
# Split gandora's index.html into ES modules — brief for a fresh agent
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
1. `HANDOFF.md` — especially **"Player rules"** (iOS invariants learned the hard way) and "What a refactor should keep".
2. `ARCHITECTURE.md` — the state machine and playback invariants.
3. `STORAGE.md` — the `db` adapter contract (chai's adapter targets it; keep the shape).
4. `tools/harness.sh`, `tools/harness.html`, `tools/harness-resume.html`, `tools/harness-cdp.py`, `tools/harness-safari.py` — these are your tests.
5. `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.js` touches `state.audio`** (or any `Audio` element that carries the station). Everything else calls its exports or listens to its events.
- `covers.js` and the generators in `render.js` are **pure** (no `document`, no `state`), so they can be checked in node.
- `brain.js` keeps working as-is: it reads `hooks`, `state`, `cssv()`, `db`; expose exactly what it needs on `window` from `app.js` (see below).
- The `venice` fetcher 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
```js
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:
1. `covers.js` + the generators (pure) → node check: `node -e` import them and draw/generate once.
2. `store.js` (db, tapes, ledger). `stats.html` and `webamp.html` must still read the same IndexedDB/localStorage shapes.
3. `dj.js` and `render.js` with the injectable fetcher.
4. `ui/*`.
5. `player.js` **last 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.
6. `app.js` wiring, `window.g`, harness fragment edits, `sw.js` list, `HANDOFF.md`/`ARCHITECTURE.md` updated 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`):
```sh
/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.html` the entry URL; keep the share link formats (`/#seed`, `/?t=`), the IndexedDB name/stores/record shapes, and all `localStorage` keys 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.