# Beat Pocket 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. 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. > Full design rationale, risk register, and V2 scope live in [`docs/PLAN.md`](./docs/PLAN.md). --- ## What it does (V1) | Screen | Treatment | |---|---| | Home (`/`) | Single-column menu, thumb-sized buttons, login chip pinned | | Lobby list | Card-per-room, full-width | | 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 | 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. --- ## Install (load unpacked) The extension is not on the Chrome Web Store yet. To try it locally: 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 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) ``` ### Runtime flow ``` [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() ``` ### 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 - 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.