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:
+274
@@ -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 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 `<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.
|
||||
Reference in New Issue
Block a user