The old README was the scaffolding-era version: said 'V1 in progress', 'no build step', 'will be pushed once vault unlocked' — all stale. New README reflects what's actually in the repo: - Repo remote, branch, smoke-test status - Real file tree under extension/content/ (6 TS modules, not 5) - Actual build pipeline (npm run build / watch / typecheck) - Smoke test section with the Playwright workflow that works - Permission surface as it actually is (storage + host permission) - Privacy stance — no data collection, ever Stale scaffolding TODOs removed. The detailed V1 architecture, risk register, and V2 backlog stay in docs/PLAN.md where they belong — README is now the entry point, not the plan.
Beat Pocket
A Manifest V3 browser extension that gives 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.
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:
- Build it (see Build below)
- Open
chrome://extensions - Toggle Developer mode (top right)
- Click Load unpacked → select the
extension/folder - 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)
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() → <div id="bbp-host"> 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:
- Stable semantic hooks the dev used (
bb-room-overlay,bb-menu-item,bb-login-chip) - Text fingerprinting (DOM nodes containing exact i18n strings like
"PLAY"/"LOG IN") - 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
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
node scripts/smoke-final.js
Requires Playwright. If it's not on the host yet:
npm i -g playwright
playwright install chromium
What the script verifies:
- Service worker registers (manifest is valid, MV3 wiring works)
- Content script runs on
https://beat-battle.net/(creates#bbp-host) - Zero page errors thrown on the live site
- 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, <all_urls>, 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.