02a90e839c
- 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
275 lines
12 KiB
Markdown
275 lines
12 KiB
Markdown
# 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.
|