- manifest.json with MV3 config, host permissions for beat-battle.net - content/detector.ts: route + overlay observer, emits bbp:phase events - content/layout.ts: defensive mobile-first layout reflow - content/chat-drawer.ts: bottom-sheet drawer, dispatches synthetic events through site's own React-managed textarea + button (preserves auth, rate-limit, mute, profanity guards) - content/util/selectors.ts: text- and role-based fallback chain - content/util/ws-proxy.ts: lightweight WebSocket instrumentation, no payload capture - content/inject.css: mobile overrides using site's existing --bb-* vars - background.js: MV3 service worker stub - package.json + tsconfig.json + esbuild.config.mjs: build pipeline - icons/: 16/32/48/128 placeholder PNGs (chili-red disc) - docs/PLAN.md: full V1 architecture, risks, scope - README.md: scope, dev workflow (load unpacked), tech
12 KiB
Beat Pocket — Plan
A browser extension (Manifest V3, Chromium + Firefox) that gives 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, splice in our own components, and relay chat through the user's existing authenticated session.
1. Why an extension (recap, briefly)
| Option | Verdict | Why |
|---|---|---|
| MV3 extension ✅ | chosen | Browser already has the session cookie, no OAuth work, zero ToS risk, ships in days |
| Separate web app scraping HTML | reject | Only public API is GET /api/online-players; everything else is server-rendered RSC behind server actions. ToS forbids scraping. |
| Native Android app | later | Same browser-restyling problem, slower to ship, Play Store friction |
| iOS Safari extension | later | MV3 extension is API-compatible; ship web first, iOS only if we hit Safari-only users |
Their robots.txt blocks AI scrapers but explicitly allows general crawlers
(User-agent: * / Allow: /), and a user-installed extension acting on the
user's own browser session is a different category from scraping.it can't be on a
blacklisted-domain list per CWS policy).
2. What we know about the site (from static analysis, no account)
- Stack: Next.js 15 App Router on Cloudflare (
x-powered-by: Next.js) - Auth:
better-auth— Google + Discord viasignIn.social({ provider, callbackURL }). Session cookie is HttpOnly, so no JS access. Endpoint pattern:/api/auth/*, server action/api/discord/join-complete. - Homepage (
/) i18n keys we found:MainMenu: { play, freeplay, ranked, custom, lobby, events, shop, profile, ... }— confirms freeplay / ranked / custom / lobby are real gameplay entry points. - In-game shell class:
bb-room-overlay— fixed inset-0 overlay that already hasoverflow-y-auto,overscroll-contain, viewport paddingpx-4 pt-6 pb-14. The author knew mobile was hard — they put overflow scrolling in. The damage is in the inner layout. - Realtime channel (extracted from the compiled JS, not from network):
- WebSocket reducer with action types
room.chat.history/room.chat.message/LOBBY_CHAT_SEND_OPTIMISTIC/RECEIVE_CHAT_HISTORY/RECEIVE_CHAT_MESSAGE - Round / phase events
room.phase∈browse | lobby | in_game - Player events
playerJoin,playerLeave,MATCH_NAVIGATION_STARTED
- WebSocket reducer with action types
- Robot policy:
search=yes, ai-train=no, use=reference— fine for us; we're not scraping with a bot, we're modifying what the user already sees. - CSS variables in use:
--bb-accent,--bb-bg,--bb-surface,--bb-border,--bb-ink,--bb-muted,--bb-muted-strong,--bb-shadow. They're already on a design-token system, which is a gift for us — we inherit their theme.
We don't know the in-game DOM structure yet (login-gated). V1 will have
to discover it dynamically with a MutationObserver.
3. Architecture
3.1 Components (5 files)
extension/
├── manifest.json # MV3, scopes & matches
├── background.js # service worker, web navigation listener
├── content/
│ ├── inject.css # mobile-first overrides for the site CSS
│ ├── detector.js # MutationObserver: finds bb-room-overlay, lobby, etc.
│ ├── layout.js # restructures outer layout: <main>, bottom drawer, FAB
│ ├── chat-drawer.js # reads chat DOM, opens WS hook, swaps a panel in
│ └── icons/ # 16/32/48/128 PNGs
└── README.md
3.2 Run-time flow
[beat-battle.net page]
│
▼
[MATCH chrome:// extension loaded on this URL]
│
inject.css applied ←────────────────────────────┐
│ │
▼ │
[detector.js] ── MutationObserver on document.body │
detects: │
• .bb-room-overlay (in-game) │
• .bb-footer-nav-main-fade (home menu) │
• route change (pushState via patchHistoryState) │
│ │
▼ │
[layout.js] runs: │
1. Hide desktop-only side panels (display:none) │
2. Re-flow main grid to 1 column │
3. Inject FAB ("💬") bottom-right for chat │
4. Mount <bbp-drawer-host> at body end │
│ │
▼ │
[chat-drawer.js] only when in-game: │
1. Snapshot DOM of chat list (clone, don't move) │
2. Wrap WebSocket to intercept {type:"room.chat.send"} messages
and re-emit through the site's own React reducer (proxy.send)
3. Render in <bbp-drawer>, swipe-up to expand │
│ │
└──── re-attach on every route change ───────┘
3.3 Selector strategy (defensive)
Beat-battle uses Tailwind — class names are stable but utility-built. We don't rely on those. We rely on:
- Stable semantic hooks the dev used:
bb-room-overlay(their own naming, low-renaming risk)bb-menu-item,bb-login-chip,bb-footer-nav-main-fadedata-attributes when we find them (they use very few — we'll add some viaMutationObserverfor the bits we need)
- Text fingerprinting as a fallback (DOM nodes containing exact i18n
strings like
"PLAY"/"CREATE ROOM"/"LOG IN WITH DISCORD") - Structural position as last resort (
body > div:nth-child(N) > …)
Every selector has a fallback chain:
const chatInput =
document.querySelector('.bb-chat-input') ||
document.querySelector('textarea[placeholder*="chat" i]') ||
document.querySelector('[aria-label*="message" i]') ||
[...document.querySelectorAll('textarea')].find(t => /chat|message|comment/i.test(t.placeholder));
4. V1 scope (the actual mobile pain)
| Screen | What we do |
|---|---|
Home (/) |
Single-column menu. Big thumb-sized buttons. Login chip pinned top-right (already in their nav, just unstick it for mobile). Hide desktop leaderboard sidebar on < 768px. |
| Lobby list | Card per lobby, full-width, drag-to-refresh. |
| In-game room | Player list at the top (horizontal scroll on mobile). Score/timer pinned to top-safe. Kit / upload / vote actions always visible (sticky bottom action bar). |
| Chat drawer | New: a bottom sheet, hidden by default, pulls up over the game when the FAB is tapped. States: closed → peeking (last message preview) → expanded (full scrollback + input). Sends through the site’s WS so messages count. |
| Settings/profile/events/shop | Out of scope for V1. Desktop-equivalent — link the user to the original page in the same tab and don't restyle. |
Things we explicitly don't do in V1:
- Affective style matching (their site is a "brutalist" red-grid; we keep it)
- Native push, voice chat, anything requiring permissions
- A settings page (no point until V2)
- iOS Safari — Chromium only for V1
5. Chat drawer — how sending works (the tricky bit)
Their chat send is React-internal: an action LOBBY_CHAT_SEND_OPTIMISTIC
that fires before the WebSocket round-trip. We can't reach into React's
state, but we can make the existing textarea + send button work in the
drawer's expanded mode:
- Read: the existing chat scrollback is a DOM node we can either:
- (a) Clone its contents into our drawer (read-only display), OR
- (b) Move the node into our drawer (write-through). We do (a) — it's less invasive and the original UI still works if the user closes the drawer.
- Send: instead of cloning the React send flow, we
- Place a
<textarea>inside our drawer - On submit, dispatch a real keyboard event on the original site's
chat textarea + a click on its submit button (or
Enterkeydown). This keeps every server-side guard intact (rate limit, mute, profanity filter).
- Place a
- As a backup, we'll wrap
WebSocket.prototype.sendat the page level and proxy theroom.chat.sendmessage type — if the UI send fails we can fall back to direct socket write. Only enable this if the dispatch- event path doesn't fire.
The drawer closes if the user navigates to another route (history change).
6. Tech choices
- Vanilla TS + esbuild, no React. A browser extension content script with React in it is doable but pointless overhead — DOM widgets only.
- No external CDN deps. Self-contained. SFX, fonts, icons bundled.
- Build pipeline:
esbuild extension/content/*.ts → dist/*.js, then zip the dist for.zipupload to Chrome Web Store. - Manifest V3 for Chromium. Firefox accepts MV3 too (Manifest V3 is fully supported in FF 109+), so a single manifest works for both.
chrome.storage.syncfor any tiny per-user prefs (chat drawer collapsed by default, etc.) — no remote config.- Permissions:
storage,*://beat-battle.net/*host permission. Notabs, nowebRequest, noscriptingfor any domain other than the one we explicitly match. (Web Store review will appreciate this minimal surface.)
7. Files & approximate LOC
extension/manifest.json 30 lines
extension/background.js 25 (very little — reload on update)
extension/content/inject.css 200 (mobile overrides)
extension/content/detector.ts 90 (route + overlay observer)
extension/content/layout.ts 220 (menu reflow, in-game shell restructure)
extension/content/chat-drawer.ts 280 (drawer + send-bridge)
extension/content/util/selectors.ts 80
extension/content/util/ws-proxy.ts 60
────────────────────────────────────
~985 LOC total
1 weekend for V1
8. V2 / future (NOT in V1)
- Spotify/YouTube kit import (if their beat-making flow accepts URL refs)
- Theme matching their
bb-accent(read CSS vars, mirror into drawer) - iOS Safari port (uses WebExtension API subset)
- Native app wrapper for Play Store
- Friend-list integration (if they expose one)
9. Risks & open questions
-
Their WS auth. A browser extension on a
.netorigin shares cookies. If chat requires the WS session cookie, our send-event will work because the same browser session is making the WS call from the page, not from our extension code. If they bind WS to a per-tab token (we couldn't confirm either way in the JS dump), we'd need to extract that token via aMutationObserveron the WS handshake. Action: log everything the page sends on first send, verify it's cookie-auth-only. -
Class name churn. Tailwind classes change on every commit. Mitigation: selectors prioritize text content, structural position, semantic attributes — not utility classes.
-
CWS review. Naming: "Beat Pocket", tiny description. Privacy disclosures: we don't collect any data (only
chrome.storage.sync, none of it leaves the device). Should be a smooth pass. -
ToS. "No reverse engineering, no automated scraping" — we're not doing either. We restyle what the user is already viewing. Will add a short statement in the privacy tab of the CWS listing making this clear.
-
Server action drift. If they change
LOBBY_CHAT_SEND_OPTIMISTIC's contract, our event-dispatch send breaks. Mitigation: the WS-proxy fallback in §5. -
iOS Safari. Not in V1; revisit after V1 ships and we have users.
10. After you approve
I'll spin up:
- The directory tree (already done —
/root/.openclaw/workspace/projects/beat-pocket/) extension/manifest.json- The five content scripts in TS
- A README with install-from-source instructions (load unpacked) so you can try it before CWS submission
- A small
dev-serveresbuild watch task + apackage.json - Smoke-test on
load unpackedusing Chromium DevTools mobile emulation
Then we can decide together whether V1 covers the real pain or whether we still need [specific thing you tell me when you test it].
No code written yet — waiting on your go.