Commit 31bbb38b authored by Mahmoud Aglan's avatar Mahmoud Aglan

docs: add WTF.md — 240 issues found across codebase audit

Full codebase scan covering security, bugs, race conditions, UX dead ends,
theming violations, dead code, and user journey problems. Includes root cause
analysis for broken friend invites and priority fix list.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 1ac45fdf
# 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.**
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment