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
This commit is contained in:
Zebratic
2026-07-11 02:24:37 +00:00
commit 02a90e839c
20 changed files with 1835 additions and 0 deletions
+274
View File
@@ -0,0 +1,274 @@
# 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: <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:
```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 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.