# Beat Pocket — Plan A browser extension (Manifest V3, Chromium + Firefox) 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, 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`](https://better-auth.com) — 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.phase` ∈ `browse | 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:
, 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 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 , 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: ```js 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: 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 `