diff --git a/README.md b/README.md index f63ff10..04e4a9d 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,162 @@ # Beat Pocket -A browser extension that gives [beat-battle.net](https://beat-battle.net) a -mobile-first UI without modifying the site itself. We never modify the -site's code or call its private API — we only restyle the DOM it gives us -and splice in our own components. +A Manifest V3 browser extension that gives [beat-battle.net](https://beat-battle.net) a mobile-first UI. We don't own the site, so we don't change it — we restyle the DOM it gives us, splice in our own components, and let chat flow through your existing authenticated session. -## Status +The session cookie is HttpOnly, the only public API is `GET /api/online-players`, and everything else is server-rendered RSC behind server actions. A user-installed extension acting on the user's own browser session is the only path that gives us a real chat send without breaking auth or scraping anything. -**V1 in progress.** See [`docs/PLAN.md`](./docs/PLAN.md) for full design. +> Full design rationale, risk register, and V2 scope live in [`docs/PLAN.md`](./docs/PLAN.md). -- [x] Plan -- [ ] Scaffolding (this commit) -- [ ] Build via Codex -- [ ] Smoke-test on Chromium "load unpacked" -- [ ] Package for Chrome Web Store +--- -## Scope (V1) +## What it does (V1) | Screen | Treatment | |---|---| -| Home (`/`) | Single-column, big thumb buttons, sticky login chip | +| Home (`/`) | Single-column menu, thumb-sized buttons, login chip pinned | | Lobby list | Card-per-room, full-width | -| In-game | Player list → top, score/timer → safe top, action bar → sticky bottom | -| Chat | NEW: bottom drawer (closed / peeking / expanded) | -| Profile / Shop / Settings / Events | **Out of scope V1** — link to original site | +| In-game room | Player list at the top, score/timer in safe area, sticky bottom action bar | +| Chat | **NEW** bottom drawer — closed / peeking / expanded. Sends through the site's own chat textarea, so the server-side rate limit, mute, and profanity filters all stay intact | +| Profile / Shop / Settings / Events | Out of scope V1 — link to original site | -Out: voice, native push, anything needing permissions. +Explicit non-goals for V1: native push, voice chat, iOS Safari, theme matching, a settings page. The site's `bb-accent` red-grid brutalist aesthetic is preserved on purpose. -## Architecture (TL;DR) +--- -1. **`inject.css`** — mobile overrides for the site's existing CSS variables -2. **`detector.ts`** — `MutationObserver` watches for `.bb-room-overlay`, - route changes -3. **`layout.ts`** — reflows outer shell, mounts drawer host -4. **`chat-drawer.ts`** — bottom sheet UI + chat send-bridge +## Install (load unpacked) -The chat-drawer sends messages by **dispatching synthetic events on the -site's own chat textarea + button**. The server-side auth, rate limit, -mute, and profanity filters all stay intact — we don't poke React state. +The extension is not on the Chrome Web Store yet. To try it locally: -## Dev (no build step in V1) +1. Build it (see [Build](#build) below) +2. Open `chrome://extensions` +3. Toggle **Developer mode** (top right) +4. Click **Load unpacked** → select the `extension/` folder +5. Visit -```sh -# Load unpacked extension -# 1. Open chrome://extensions -# 2. Toggle Developer Mode -# 3. "Load unpacked" -> select extension/ -# 4. Visit https://beat-battle.net +For Firefox 109+: `about:debugging#/runtime/this-firefox` → **Load Temporary Add-on** → pick `extension/manifest.json`. + +After editing TypeScript, run `npm run build` again and hit the refresh icon on the extension card in `chrome://extensions`. + +--- + +## Architecture + +``` +extension/ +├── manifest.json # MV3, scope = *://beat-battle.net/* +├── background.js # MV3 service worker stub +├── content/ +│ ├── index.ts # entry — wires the rest together +│ ├── detector.ts # route + overlay observer → bbp:phase events +│ ├── layout.ts # ensureHost + per-phase layout reflow +│ ├── chat-drawer.ts # bottom sheet + send-bridge +│ ├── inject.css # mobile overrides using site --bb-* vars +│ └── util/ +│ ├── selectors.ts # defensive selector chains +│ └── ws-proxy.ts # lightweight WS instrumentation +├── icons/ # 16/32/48/128 PNGs +└── dist/content/index.js # esbuild output (gitignored) ``` -For Firefox: `about:debugging#/runtime/this-firefox` → "Load Temporary -Add-on" → pick `extension/manifest.json`. +### Runtime flow -## Tech +``` +[beat-battle.net page] + │ + ▼ +content script injected at document_idle + │ + ├── inject.css applied (mobile overrides using site CSS vars) + │ + ├── detector.startDetector() + │ • patches history.pushState / replaceState / popstate + │ • MutationObserver on document.documentElement + │ • emits `bbp:phase` ∈ {home, lobby-list, login, in-game, unknown} + │ + ├── layout.ensureHost() →
on body + │ + └── ws-proxy.instrumentWS() — observes WS lifecycle, no payload capture + │ + ▼ + `bbp:phase` listener: + home / lobby-list → applyHomeLayout() [hide sidebar, full-width main] + in-game → applyInGameLayout() + mountDrawer() + login / unknown → unmountDrawer() +``` -- Vanilla TypeScript -- esbuild for the bundle (one-time build, no HMR for V1) -- MV3 (works in Chromium + Firefox 109+) +### Chat send — the part that matters + +The site's chat send is React-internal. We can't poke React state, but the textarea and submit button are real DOM nodes — so we **dispatch synthetic keyboard/click events on the original textarea** from inside our drawer's expanded mode. The browser session, the cookie, the rate limit, the mute, the profanity filter: all of it stays exactly where the site put it. As a fallback, `ws-proxy.ts` wraps `WebSocket.prototype.send` so we can re-emit a `room.chat.send` if the event-dispatch path fails. + +### Selector strategy + +Tailwind classes change every commit, so we never rely on them. Every selector has a fallback chain: + +1. Stable semantic hooks the dev used (`bb-room-overlay`, `bb-menu-item`, `bb-login-chip`) +2. Text fingerprinting (DOM nodes containing exact i18n strings like `"PLAY"` / `"LOG IN"`) +3. Structural position as last resort + +If the site renames a class, we degrade gracefully — the worst case is the original (bad) mobile UI showing through. + +--- + +## Build + +```sh +npm install # esbuild + typescript + @types/chrome +npm run build # one-shot esbuild → extension/dist/content/index.js +npm run watch # rebuild on save +npm run typecheck # tsc --noEmit +``` + +`extension/dist/` is gitignored. Chrome loads `extension/dist/content/index.js` per the manifest; the build must complete before "Load unpacked" will work. + +--- + +## Smoke test + +```sh +node scripts/smoke-final.js +``` + +Requires Playwright. If it's not on the host yet: + +```sh +npm i -g playwright +playwright install chromium +``` + +What the script verifies: + +1. Service worker registers (manifest is valid, MV3 wiring works) +2. Content script runs on `https://beat-battle.net/` (creates `#bbp-host`) +3. Zero page errors thrown on the live site +4. The original UI still renders (PLAY button visible) + +The other scripts in `scripts/` (`probe.js`, `probe-inject.js`, `probe-minimal.js`, `smoke.js`) are diagnostic — they were used to narrow down a headless-Chrome `--load-extension` quirk and are kept for future regression hunting. + +**Known limitation:** the smoke test runs against the logged-out homepage. The drawer is gated on `phase === 'in-game'`, which requires a Discord/Google login. To verify the chat send bridge end-to-end, load unpacked in your own browser, log in, click PLAY, and check the drawer. + +--- ## Repo -This project will be pushed to `git.molberg.cloud/bonzi/beat-pocket` once -the vault session is unlocked. +- Remote: `https://git.molberg.cloud/bonzi/beat-pocket` +- Branch: `main` +- Status: V1 scaffold complete, smoke test green on homepage + +## Permissions + +Minimal on purpose — Chrome Web Store review should be a smooth pass: + +- `storage` — for per-user drawer prefs (`chrome.storage.sync`, never leaves the device) +- `*://beat-battle.net/*` — host permission, the only site we touch + +We do **not** request `tabs`, `webRequest`, `scripting`, ``, or any cross-origin capability. + +## Privacy + +We don't collect anything. No telemetry, no analytics, no remote config, no third-party CDN. The only network requests the extension makes are the same ones the user would make opening the site themselves. The site sets its own cookies and we don't read, modify, or forward them. ## License -Private until first public release. +Private until first public release. \ No newline at end of file