# WTF Counter — el3ab-player

**Date:** 2026-07-03
**Total WTFs:** 240
**Critical:** 31 | **Major:** 93 | **Minor:** 116

---

## Summary

| Area | Critical | Major | Minor | Total |
|------|----------|-------|-------|-------|
| API Backend (PHP) | 13 | 19 | 12 | 44 |
| Core Modules (JS) | 6 | 22 | 37 | 65 |
| Chess Module | 4 | 12 | 20 | 36 |
| Play/Social/Auth Scenes | 5 | 18 | 17 | 40 |
| Other Games + Profile + Rewards | 3 | 22 | 30 | 55 |

---

## WHY FRIEND INVITES DON'T WORK (3 converging bugs)

1. **multiplayer.js:79** — When `addFriendFromGame` returns `false` (network failure), code shows "already sent" instead of error. User thinks invite was sent but it wasn't.
2. **chess game.js:22 + lobby.js:175** — Color assignment broken: lobby passes `color: params.color` which may be undefined from `friends.php`. Both players default to `'w'` (white), so neither can move as black.
3. **lobby.js:132-133** — Non-host guest immediately enters game before host detects acceptance (0-3.5s desync window). One player is in-game while other is still in lobby.

---

## CRITICAL (31)

### Security — Any User Can Tamper With Any Match

| # | File | Line | Issue |
|---|------|------|-------|
| 1 | api/game.php | 107-161 | `handleGameMove` has NO auth check — any user can modify any match's FEN/moves |
| 2 | api/game.php | 164-186 | `handleResign` never validates caller is a participant — unrelated user triggers `white_wins` |
| 3 | api/game.php | 210-249 | `handleComplete` has NO auth check — any user can complete any match and steal rewards |
| 4 | api/game.php | 215 | `$winners` array from client input passed unchecked — client can claim arbitrary user IDs as winners |
| 5 | api/friends.php | 506-581 | `accept-invite` never verifies accepting user is the target — anyone with match_id can accept |
| 6 | api/friends.php | 583-606 | `decline-invite` never verifies user is the invited target |

### Security — Secrets & CORS

| # | File | Line | Issue |
|---|------|------|-------|
| 7 | config/constants.php | 4-5 | `SUPABASE_SERVICE_KEY` (admin bypass) hardcoded in source — repo leak = all data compromised |
| 8 | config/constants.php | 3 | `SUPABASE_ANON_KEY` hardcoded in tracked file |
| 9 | All API files | 4 | `Access-Control-Allow-Origin: *` on every endpoint — CSRF via CORS |
| 10 | api/friends.php | 76 | Search query interpolated into PostgREST filter — filter injection |
| 11 | api/friends.php | 88 | `$_GET['ids']` passed directly into `in.()` filter — filter injection |
| 12 | api/auth.php | 129-187 | Guest account creation with no rate limiting — unlimited spam accounts |
| 13 | api/game.php | 408-462 | `find-active-match` accepts arbitrary `player_id` — info disclosure |
| 14 | login.js + register.js | 89-90 | Google OAuth has no `state` parameter or PKCE — CSRF on callback |

### Game-Breaking Bugs

| # | File | Line | Issue |
|---|------|------|-------|
| 15 | multiplayer.js | 79-83 | Friend request failure shows "already sent" success — silent failure |
| 16 | multiplayer.js | 148-155 | Emotes sent via `action: 'move'` overwriting `game_state` — corrupts match state |
| 17 | multiplayer.js | 210-219 | Rematch request also overwrites `game_state` — same corruption risk |
| 18 | chess/game.js | 22 + lobby.js:175 | Friend invite color undefined → both players see white → nobody can move as black |
| 19 | chess/logic/live.js | 1-94 | Entire file is unused dead code (old Supabase realtime approach) |
| 20 | chess/game.js | 913 | `matchLive.session.destroy()` incorrect call — may throw or fail silently |
| 21 | chess/game.js | 588 | `lastKnownMoveCount` module-level — stale state persists between games |
| 22 | modal.js | 9 | Only one `resolvePromise` stored globally — stacked modals hang forever |
| 23 | net.js | 22 | `res.json()` called unconditionally — HTML error pages throw cryptic SyntaxError |
| 24 | match-session.js | 175-179 | `currentSession` null dereference in visibility callback after `destroy()` |
| 25 | ludo/game.js | 1442 | `isLoser` never defined — win celebration fires even on loss |
| 26 | backgammon/rules.js | 258 | `VARIANTS.standard` doesn't exist — crash on gammon/backgammon scoring |
| 27 | backgammon/rules.js | 269 | `getPipCount` hardcodes sheshbesh variant — wrong for mahbousa/thirtyone |
| 28 | lobby.js | 132-133 | Guest enters game immediately before host detects — desync window |
| 29 | lobby.js | 165 | `setTimeout(startGame, 1500)` fires even after navigation — pushes scene onto wrong stack |
| 30 | lobby.js | 129+136 | Poll with no concurrency guard — slow network triggers `startGame` multiple times |
| 31 | chat.js | 225 | `JSON.parse(msg.metadata)` with no try/catch — one bad message crashes entire list |

---

## MAJOR (93)

### Race Conditions & State Corruption

| # | File | Line | Issue |
|---|------|------|-------|
| 32 | api/multiplayer.php | 41-43 | `join_room` read-then-write on players array — simultaneous joins overwrite each other |
| 33 | api/matchmaking.php | 148-166 | Match insert success + null matchId → opponent stuck in "matched" forever |
| 34 | queue.js | 81-88 | `pollForMatch` uses setInterval with async — overlapping in-flight requests |
| 35 | match-session.js | 104-105 | `onConnectionRestored` fires on EVERY successful poll, not only after disconnect |
| 36 | match-session.js | 147-155 | Opponent jumps from active to abandoned without disconnect warning |
| 37 | match-session.js | 160-186 | `visibilitychange` listener never removed on `destroy()` — memory leak |
| 38 | net.js + match-session.js | — | Overlapping 401s trigger dual token refresh — single-use refresh token invalidates itself |
| 39 | chess/game.js | 812 | `lastDrawOfferHandled` module-level persists across games |
| 40 | chess/game.js | 282-293 | Recovery and live setup conflict on clock state |
| 41 | domino/game.js | 609 | `syncPassToServer` pre-increments moveCount before network call — desync on failure |
| 42 | domino/game.js | 837 | `drawFromBoneyard` rapid syncs in while loop overwrite each other |
| 43 | rewards/achievements.js | 36-44 | Recursive re-mount while still rendering |
| 44 | shop/browse.js | 90 | Post-purchase local balance ignores server response — concurrent purchases show wrong amount |

### Broken Game Logic

| # | File | Line | Issue |
|---|------|------|-------|
| 45 | api/multiplayer.php | 37 | Default table is `ludo_matches` for non-domino games — chess writes to wrong table |
| 46 | api/game.php | 198-204 | `handleDraw` overwrites entire `game_state` with `{draw_accepted: true}` — destroys all state |
| 47 | chess/game.js | 926-927 | `opponentRating` uses botElos even in live mode — wrong rating shown |
| 48 | chess/game.js | 19 | Module-level singletons never cleaned — old clock timers keep firing on re-mount |
| 49 | chess/game.js | 514-516 | Bot move retry has no limit — infinite 2s loop if API is down |
| 50 | chess/game.js | 289 | `onGameEnd` always returns 'loss'+'abandon' regardless of actual result |
| 51 | chess/canvas/board.js | 506-513 | `resize()` uses 400px max but `setup` uses 500px — board shrinks permanently |
| 52 | chess/analysis.js | 13 | Analysis shares engine singleton with game — mounting both corrupts state |
| 53 | chess/result.js | 171-174 | Client optimistically credits coins — server may double-credit |
| 54 | chess/result.js | 135 | Rematch always sends `rapid_10_0` regardless of actual time control used |
| 55 | ludo/game.js | 388-389 | Bot indicator grabs wrong `<span>` element — corrupts turn arrow text |
| 56 | backgammon/game.js | 628 | `Object.assign(game, data.game_state)` clobbers all local properties |
| 57 | profile/view.js | 354-359 | Spectate routes domino/backgammon to `chess-spectate` — wrong game shown |
| 58 | tournaments/hub.js | 206 | "Play Now" hardcodes `chess-game` regardless of tournament's game_key |
| 59 | shop/browse.js | 64 | `canAfford` only checks coins, ignores gems — players buy gem items for free |
| 60 | shop/browse.js | 66 | `document.getElementById('overlay')` assumes element exists — crash if missing |

### UX Dead Ends & Broken Flows

| # | File | Line | Issue |
|---|------|------|-------|
| 61 | table.js | 383 | "My Games" always navigates to chess-history regardless of selected game |
| 62 | table.js | 271 | Animation references undefined `slideUpBounce` keyframes |
| 63 | friends.js | 73-77 | Timers never cleaned up — re-mounting stacks intervals indefinitely |
| 64 | friends.js | 182 | "Groups" tab pushes new scene instead of loading inline — breaks tab pattern |
| 65 | notifications.js | 33-38 | Notification items have NO click handlers — dead-end display |
| 66 | notifications.js | 20 | `unreadCount = 0` on mount — marks all read before user sees them |
| 67 | queue.js | 74-76 | Failed `joinQueue` silently pops scene — zero error feedback |
| 68 | chat.js | 246 | `if (scrollToBottom || true)` — always yanks to bottom, user can't read history |
| 69 | chat.js | 132-133 | Poll fetches full history every 3s — `lastTime` computed but never sent |
| 70 | scene.js | 47 | `push()` silently drops navigation during 300ms transition — lost user intent |
| 71 | scene.js | 97 | `innerHTML = ''` without calling unmount on current scene — leaks intervals/listeners |
| 72 | scene.js | 86-90 | If new scene mount() throws, user sees blank screen with no recovery |
| 73 | multiplayer.js | 97 | Empty catch on mute — no error feedback |
| 74 | multiplayer.js | 115 | Empty catch on block — no error feedback |
| 75 | multiplayer.js | 118-121 | Report action fires with one tap, no confirmation dialog |
| 76 | backgammon/game.js | 688 | Quit has no confirmation — accidental tap loses match |
| 77 | backgammon/game.js | 655 | Chat messages shown locally but never synced — opponent can't see them |
| 78 | profile/settings.js | 93-105 | Delete account modal opens then fetches — incomplete UI while loading |
| 79 | rewards/daily.js | 29-36 | UTC date comparison — wrong claim status for non-UTC timezone users |
| 80 | shop/browse.js | 88 | No "already owned" indicator — items remain purchasable forever |

### Architecture & Performance

| # | File | Line | Issue |
|---|------|------|-------|
| 81 | api/friends.php | 446-472 | `check-invites` queries ALL waiting domino matches — O(N) table scan |
| 82 | api/friends.php | 475-500 | Same full-table-scan for ludo invites |
| 83 | api/chat.php | 57-84 | N+1 query: separate DB call per friendship for unread counts |
| 84 | api/chat.php | 87-121 | N+1 query: separate DB call per friendship for last message |
| 85 | includes/auth.php | 30-46 | Ban check DB query on EVERY authenticated request |
| 86 | includes/auth.php | 63-66 | `getUserId()` calls `verifyToken()` again — double HTTP call per request |
| 87 | multiplayer.js + match-session.js | — | Two competing disconnect detection systems with different thresholds |
| 88 | match-session.js + realtime.js | — | HTTP polling every 2s even though WebSocket exists — redundant traffic |
| 89 | friends.js | 93-99 | `checkInvites()` fires 2 requests every 5 seconds — no caching |
| 90 | challenge.js + friends.js + chat.js | — | Invite dialog copy-pasted 3 times with diverging behaviors |
| 91 | lobby.js | 8-10 | Module-level mutable state shared across instances — navigation corruption |
| 92 | profile/view.js | 117 | Avatar upload bypasses `net` module — skips auth refresh/error handling |
| 93 | realtime.js | 37-44 | Only handles `postgres_changes` — subscription failures completely silent |

### Backend Logic Bugs

| # | File | Line | Issue |
|---|------|------|-------|
| 94 | api/auth.php | 260-261 | `handleDeleteAccount` only deletes profile — orphans all related data |
| 95 | api/friends.php | 299-398 | Invite creates match immediately but no notification for decline/expiry |
| 96 | api/game.php | 275-305 | Tournament report uses `$_SERVER['HTTP_HOST']` (spoofable) + `@file_get_contents` (silent failure) |
| 97 | api/game.php | 392 | `array_is_list()` requires PHP 8.1+ — fatal on older versions |
| 98 | api/multiplayer.php | 23 | Room code has no uniqueness check — collision creates broken state |
| 99 | multiplayer.js | 232-237 | Friend request error matching relies on exact English substrings |
| 100 | domino/game.js | 396 | `dealAndSyncToServer` swallows sync error — player 2 never gets tiles |

### Hardcoded UI (Theming Violations)

| # | File | Line | Issue |
|---|------|------|-------|
| 101 | multiplayer.js | 27, 60 | Inline colors throughout opponent menu |
| 102 | chess/game.js | 103-173 | Massive inline HTML with hardcoded colors |
| 103 | challenge.js | 20-36, 167 | Full CSS with hardcoded colors |
| 104 | lobby.js | 91-114 | Full CSS with hardcoded colors |
| 105 | modal.js | 33-37, 96-110 | Modal container/buttons all hardcoded |
| 106 | realtime.js | 2, 5 | URL and anon key hardcoded — no env switching |

### Misc Major

| # | File | Line | Issue |
|---|------|------|-------|
| 107 | multiplayer.js | 181-203 | `startDisconnectWatch` duplicates match-session.js logic |
| 108 | chess/game.js | 186 | Board `canSelect` uses stale closure after recovery color change |
| 109 | chess/clock.js | 18-29 | `start()` can produce negative drift on loaded devices |
| 110 | lobby.js (guest) | 130-133 | Guest calls `startGame()` without verifying match exists or is in_progress |
| 111 | api/matchmaking.php | 40-43 | Anti-abuse returns 200 with `{error}` instead of HTTP 429 |
| 112 | includes/supabase.php | 13-28 | `supabaseRpc` returns null on failure — callers don't null-check |
| 113 | backgammon/game.js | 48 | `params.opponentName` injected into innerHTML — XSS vector |
| 114 | friends.js | 341 | Profile JSON in `data-profile` attribute — potential XSS |
| 115 | api/matchmaking.php | 59 | Rating falls back to `elo_rapid` for all time controls |
| 116 | rewards/daily.js | 10 | `DAY_REWARDS` hardcoded client-side — diverges from server |

---

## MINOR (116)

### i18n / Hardcoded Arabic

| # | File | Line | Issue |
|---|------|------|-------|
| 117 | multiplayer.js | 36 | Fallback opponent name `'خصم'` not using `t()` |
| 118 | multiplayer.js | 62-67 | Mixed i18n: some calls use `t()`, others are raw Arabic |
| 119 | chess/game.js | 239-240 | Draw rejection message Arabic-only |
| 120 | chess/game.js | 253-257 | Draw offer sent message Arabic-only |
| 121 | chess/result.js | 25-32 | Reason text translations are inline Arabic |
| 122 | ludo/game.js | 32 | `PLAYER_NAMES` array uses Arabic strings not through `t()` |

### Hardcoded Emojis (Theming Rule Violations)

| # | File | Line | Issue |
|---|------|------|-------|
| 123 | table.js | 36 | `👋` greeting emoji not using `emoji()` |
| 124 | table.js | 43 | Quick-action gradients use hardcoded hex |
| 125 | multiplayer.js | 63 | `➕` emoji hardcoded |
| 126 | domino/game.js | 112-113 | `👤`/`🤖` hardcoded |
| 127 | domino/game.js | 124 | `📦` boneyard icon hardcoded |
| 128 | domino/game.js | 166 | `😄` emote button hardcoded |
| 129 | domino/game.js | 662 | Emote map uses raw emoji strings |
| 130 | ludo/game.js | 136 | `✕` and `😄` in buttons |
| 131 | ludo/game.js | 471 | Bot emote array raw emojis |
| 132 | ludo/game.js | 1141 | `⚡` turbo mode |
| 133 | rewards/achievements.js | 143 | `tierIcon()` uses `⭐🌟💫👑` |
| 134 | rewards/achievements.js | 118 | Achievement card colors hardcoded |
| 135 | shop/browse.js | 41 | All items show same `🎨` emoji |

### Hardcoded Values / Magic Numbers

| # | File | Line | Issue |
|---|------|------|-------|
| 136 | multiplayer.js | 37 | Default rating `1200` magic number |
| 137 | chess/game.js | 108 | Bot portrait URL hardcoded to stockfishapi domain |
| 138 | chess/game.js | 235 | Bot draw acceptance `Math.random() < 0.5` — not personality-aware |
| 139 | chess/game.js | 922 | Coins/XP rewards hardcoded (win:50, draw:20, loss:10) |
| 140 | chess/game.js | 957-960 | Failsafe rating values (+12, +1, -8) hardcoded |
| 141 | time-select.js | 6-43 | Category colors and backgrounds hardcoded |
| 142 | domino/game.js | 179 | Background colors hardcoded |
| 143 | ludo/game.js | 48 | `TURN_TIMEOUT = 15000` not configurable |
| 144 | ludo/game.js | 627 | Polling interval `1500`ms magic number |
| 145 | ludo/game.js | 384 | Turbo multiplier `0.4` magic number |
| 146 | ludo/game.js | 1297 | Dice shake interval `55`ms magic number |
| 147 | login.js | 9 | `SUPABASE_URL` duplicated in login + register |
| 148 | api/game.php | 294-295 | Tournament URL from `HTTP_HOST` — breaks behind proxy |

### Dead Code

| # | File | Line | Issue |
|---|------|------|-------|
| 149 | table.js | 59 | `const disabled = false` — can never trigger disabled styling |
| 150 | ludo/logic/live.js | 1-44 | Entire module dead — game.js uses own polling |
| 151 | api/game.php | 251-266 | `calculateElo`/`getRatingColumn`/`getTimeControlType` never called |
| 152 | match-session.js | 12 | `RECONNECT_GRACE = 5000` defined but never used |
| 153 | scene.js | 99 | `isBack` parameter received but never used |

### UX Polish Issues

| # | File | Line | Issue |
|---|------|------|-------|
| 154 | time-select.js | 71 | `:hover` on touch device — no effect |
| 155 | friends.js | 655 | Error shows "no news" — misleads instead of reporting failure |
| 156 | queue.js | — | No timeout on matchmaking queue — timer counts forever |
| 157 | hud.js | 94-101 | Guest sees 4 locked tabs — tease UX wasting space |
| 158 | hud.js | 165-169 | Coin animation and count update fire simultaneously |
| 159 | profile/view.js | 478 | Unrated players show `1200` instead of "unrated" |
| 160 | profile/settings.js | 77 | Sound plays after user toggles audio OFF |
| 161 | profile/view.js | 25 | Profile fetches fresh data every mount — no cache |
| 162 | rewards/achievements.js | 33 | Fetch error shows empty page — no retry option |
| 163 | chess/analysis.js | 104-106 | Analysis jumps to final position — no navigation |
| 164 | challenge.js | 210 | Chat invite notification failure silenced |
| 165 | notifications.js | 18 | Bad API shape turns entire response into "notifications" |
| 166 | lobby.js | 86 | "Start" button visible for guest but auto-start already fires |
| 167 | tournaments/hub.js | 113 | Draft tournaments visible to regular players |
| 168 | shop/browse.js | — | No pagination — all items load at once |

### Minor Bugs

| # | File | Line | Issue |
|---|------|------|-------|
| 169 | multiplayer.js | 130-133 | Outside-click listener never cleaned up on scene transition |
| 170 | multiplayer.js | 164 | `JSON.parse(gameState)` no try/catch |
| 171 | net.js | 63 | Variable `refreshToken` shadows function name |
| 172 | net.js | 77 | Token refresh failure silently swallowed |
| 173 | net.js | 25-34 | No handling for 403/429/5xx — all generic errors |
| 174 | net.js | 44 | Query param keys not URI-encoded |
| 175 | net.js | 6-38 | No request timeout — hung server = permanent loading |
| 176 | store.js | 49 | Deep path set overwrites intermediate non-object values |
| 177 | store.js | 20-26 | Shallow merge loses new nested keys from stale persisted data |
| 178 | store.js | 29-39 | `persist()` on every `set()` — no debouncing |
| 179 | hud.js | 134 | Toast timeout not cleared on button click |
| 180 | bus.js | — | No `removeAllListeners()` for session reset |
| 181 | realtime.js | 57-58 | Reconnect races with manual disconnect |
| 182 | realtime.js | 78 | No duplicate callback detection — fires multiple times |
| 183 | realtime.js | 88 | `leaveChannel` dropped when WebSocket disconnected |
| 184 | realtime.js | 130 | Topic split truncates filters with colons |
| 185 | realtime.js | 30-31 | Re-join on reconnect doesn't wait for confirmation |
| 186 | realtime.js | 21 | Auth token in WebSocket URL query string — visible in logs |
| 187 | match-live.js | 25-28 | `lastMoveCount` tracked but never gates anything |
| 188 | match-live.js | 81-88 | `recover()` misleading name — doesn't actually reconnect |
| 189 | match-session.js | 95,131,174 | Endpoint ternary duplicated 3 times |
| 190 | scene.js | 91 | `isTransitioning` released by hardcoded 300ms regardless of actual animation |
| 191 | modal.js | 144-150 | Button click doesn't remove backdrop listener |
| 192 | modal.js | 47 | Forced reflow for animation — jank on low-end devices |
| 193 | chess/canvas/board.js | 21 | Singleton — only one board instance possible |
| 194 | chess/logic/engine.js | 4 | `Chess()` assumes global — no import, relies on script order |
| 195 | chess/game.js | 644 | `game_state` parsed multiple independent times per poll |
| 196 | chess/game.js | 332 | `bus.emit('game:started')` fires before live session initialized |
| 197 | chess/game.js | 437-439 | Clock only starts on moveCount===1 — recovery misses this |
| 198 | chess/clock.js | 80-86 | `bullet_0_1` returns `time: 0` |
| 199 | chess/canvas/board.js | 284-287 | No listener cleanup on destroy — memory leak during drag |
| 200 | chess/canvas/board.js | 265-278 | `clientHeight` may be 0 before layout — falls back to magic numbers |
| 201 | login.js | 72 | `audio.play('coin', 'reward')` — unclear second argument |
| 202 | login.js | 93 | Guest handler shadows outer `btn` variable |
| 203 | register.js | 16 | Username validation HTML-only — no regex for special chars |
| 204 | friends.js | 11 | `activeTab` module-level leaks between mounts |
| 205 | queue.js | 11-16 | `unmountQueue()` async `leaveQueue()` without await |
| 206 | chat.js | 8-12 | Module-level mutable state — corruption on re-mount |
| 207 | chat.js | 115 | 3s polling instead of WebSocket |
| 208 | domino/game.js | 949 | Non-cancellable timeout accesses stale state on unmount |
| 209 | domino/game.js | 1025 | `winners` uses potentially empty `state.players` |
| 210 | ludo/game.js | 687-690 | Document listener never cleaned up on unmount |
| 211 | backgammon/game.js | 141 | `window.resize` listener leaks on crash |
| 212 | backgammon/game.js | 692-867 | CSS injected as raw `<style>` on every mount |
| 213 | domino/game.js | 174-336 | 160 lines CSS injected via JS on every mount |
| 214 | backgammon/logic/rules.js | 377-379 | Catch-all swallows all exceptions — hides move bugs |
| 215 | rewards/daily.js | 118-119 | `juice.coinFlyTo` targets `#hud-coins` which may not exist |
| 216 | profile/settings.js | 52 | Duplicate `display:none` in inline styles |

### Backend Minor

| # | File | Line | Issue |
|---|------|------|-------|
| 217 | api/matchmaking.php | 125 | `rand(0,1)` not cryptographically fair for rated games |
| 218 | api/friends.php | 315-398 | 120s invite timeout — offline players can never see invite |
| 219 | api/notifications.php | 36-49 | PATCH falls through to 405 for unknown actions |
| 220 | api/ratings.php | 17 | Any user can query anyone's rating history |
| 221 | api/auth.php | 50 | Password only checks length >= 6 |
| 222 | api/auth.php | 55 | Username no invalid char / profanity check |
| 223 | api/auth.php | 42-44 | No email format validation |
| 224 | api/friends.php | 221-233 | Report action no rate limiting |
| 225 | api/matchmaking.php | 59 | Rating fallback to rapid for all queues |
| 226 | config/database.php | 111 | Curl failure masked as empty success |

### Cross-Cutting Architecture (Minor)

| # | File | Line | Issue |
|---|------|------|-------|
| 227 | multiplayer.js | 69-70 | `<style>` injected into DOM every menu open |
| 228 | bus.js | 4-5 | Empty arrays accumulate for unsubscribed events |
| 229 | match-live.js | — | Redundant with match-session.js |
| 230 | chess/game.js | 644 | Triple-parse of same game_state per poll |

---

## USER JOURNEY PROBLEMS (Not Bugs, But Bad UX)

| # | Issue | Impact |
|---|-------|--------|
| 231 | No onboarding — new user lands on game grid with no guidance | Users don't know what to do |
| 232 | Friend invite flow: no real-time notification — relies on 5s polling | Invites feel laggy or invisible |
| 233 | Matchmaking queue has no timeout — user waits forever | Users think app is frozen |
| 234 | Notifications are display-only — can't navigate to source | Dead end, user can't act on them |
| 235 | Chat yanks to bottom every 3s — can't scroll up to read history | Unusable for long conversations |
| 236 | Game result screen shows hardcoded ratings — may not match server | Confusing/misleading feedback |
| 237 | Guest sees locked tabs everywhere — frustrating tease, no clear CTA to sign up | Conversion killer |
| 238 | "My Games" always goes to chess history even if playing domino | Broken cross-game UX |
| 239 | Tournament "Play Now" always launches chess regardless of game type | Non-chess tournaments unplayable |
| 240 | Shop has no "owned" state — can re-purchase items | Confusing and potentially exploitable |

---

## TOP 10 FIXES (Priority Order)

1. **Auth checks on game.php endpoints** — any user can corrupt any match right now
2. **Fix friend invite color assignment** — both players get white, game is stuck
3. **Fix lobby desync** — add handshake before entering game
4. **Move secrets to env vars** — service key in source code is a ticking bomb
5. **Fix emote/rematch state corruption** — don't overwrite game_state for non-move actions
6. **Restrict CORS** — `*` allows any site to make authenticated requests
7. **Fix `isLoser` in ludo** — win celebration on every game end
8. **Fix backgammon VARIANTS.standard crash** — game scoring is broken
9. **Add concurrency guards to polling** — multiple game-breaking race conditions
10. **Fix modal stacking** — Promise hangs forever if two modals overlap

---

## WTF DENSITY (issues per file)

| File | WTFs | Lines | Density |
|------|------|-------|---------|
| chess/scenes/game.js | 22 | ~960 | 1 per 44 lines |
| api/game.php | 14 | ~460 | 1 per 33 lines |
| api/friends.php | 12 | ~610 | 1 per 51 lines |
| multiplayer.js | 18 | ~240 | 1 per 13 lines |
| lobby.js | 8 | ~170 | 1 per 21 lines |
| ludo/scenes/game.js | 11 | ~1450 | 1 per 132 lines |

**Worst file: `multiplayer.js` — 1 WTF every 13 lines.**
