Files
Zebratic 02a90e839c Initial scaffold: Beat Pocket MV3 extension
- 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
2026-07-11 02:24:37 +00:00

12 KiB
Raw Permalink Blame History

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 via signIn.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 has overflow-y-auto, overscroll-contain, viewport padding px-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.phasebrowse | lobby | in_game
    • Player events playerJoin, playerLeave, MATCH_NAVIGATION_STARTED
  • 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:

  1. 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-fade
    • data- attributes when we find them (they use very few — we'll add some via MutationObserver for the bits we need)
  2. Text fingerprinting as a fallback (DOM nodes containing exact i18n strings like "PLAY" / "CREATE ROOM" / "LOG IN WITH DISCORD")
  3. 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 sites 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:

  1. 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.
  2. 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 Enter keydown). This keeps every server-side guard intact (rate limit, mute, profanity filter).
  3. As a backup, we'll wrap WebSocket.prototype.send at the page level and proxy the room.chat.send message 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 .zip upload 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.sync for any tiny per-user prefs (chat drawer collapsed by default, etc.) — no remote config.
  • Permissions: storage, *://beat-battle.net/* host permission. No tabs, no webRequest, no scripting for 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

  1. Their WS auth. A browser extension on a .net origin 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 a MutationObserver on the WS handshake. Action: log everything the page sends on first send, verify it's cookie-auth-only.

  2. Class name churn. Tailwind classes change on every commit. Mitigation: selectors prioritize text content, structural position, semantic attributes — not utility classes.

  3. 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.

  4. 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.

  5. Server action drift. If they change LOBBY_CHAT_SEND_OPTIMISTIC's contract, our event-dispatch send breaks. Mitigation: the WS-proxy fallback in §5.

  6. iOS Safari. Not in V1; revisit after V1 ships and we have users.


10. After you approve

I'll spin up:

  1. The directory tree (already done — /root/.openclaw/workspace/projects/beat-pocket/)
  2. extension/manifest.json
  3. The five content scripts in TS
  4. A README with install-from-source instructions (load unpacked) so you can try it before CWS submission
  5. A small dev-server esbuild watch task + a package.json
  6. Smoke-test on load unpacked using 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.