1. 19 Oct, 2026 27 commits
    • Mahmoud Aglan's avatar
      feat(game): in-game shell and domino in Night Arcade; invite fixes · e4ccd12e
      Mahmoud Aglan authored
      Shell (match UI, player plates, dialogs, reactions, queue, VS, challenge):
      flat Night Arcade surfaces, turn timers on the plates, banners for your
      turn / time low / waiting, a claimable disconnect ring, and a cancel that
      waits for an in-flight join so it never leaves a ghost queue row.
      tests/play/shell.mjs: 144/147 (the 3 were the invite bugs below).
      
      Domino: flat white tiles on a lip, a purple-tinted table, playable tiles
      lifted, the hand fanned with translate. tests/play/domino.mjs: 785/785
      against the bot, scripted positions and online.
      
      friends.php: a ludo invite wrote a column ludo_matches does not have (400)
      and opened a four-seat table; domino and ludo invites stored players as a
      JSON string, so check-invites never showed them to the friend.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      e4ccd12e
    • Mahmoud Aglan's avatar
      fix(reactions): backgammon players can send reactions in online games · b789888e
      Mahmoud Aglan authored
      isMatchParticipant() knew only ludo and domino matches, so every reaction
      poll in an online backgammon game answered 403 and nothing reached the
      opponent. Backgammon seats come from players or the colour columns.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      b789888e
    • Mahmoud Aglan's avatar
      fix(hud): fits 360px phones; FIDE title strings · 0fa3571d
      Mahmoud Aglan authored
      On narrow phones the purse keeps its tap but drops its plus key and the
      name tightens, so the bell no longer falls off the screen edge. The eight
      FIDE title labels on the profile editor were missing.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      0fa3571d
    • Mahmoud Aglan's avatar
      feat(design): Night Arcade, a modern minimal game UI replaces Royal Cafe · a20507ed
      Mahmoud Aglan authored
      The owner rejected the walnut and leather look as dated. The new world:
      a quiet violet-black ink stage with a faint star lattice; flat ink tiles
      for lists (each row its own tile); white game cards on a lip; flat colour
      buttons on a solid lip the finger pushes down; a floating HUD and a
      floating dock with the green play key standing out of it; Rubik, self-
      hosted, with the display face pinned to ExtraBold. Stickers are rebuilt
      flat on a lip, the game art is redrawn flat, each game owns one hue.
      
      Home, HUD, rewards, auth, shop, settings, profile and the leaderboard are
      converted; game screens follow.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      a20507ed
    • Mahmoud Aglan's avatar
      fix(matchmaking): chess and domino players who queue together are matched · c227bce1
      Mahmoud Aglan authored
      Chess: a status poll that landed between the claim and the match id write
      deleted the claim, so the waiting player searched forever while the other
      sat alone. A claim in progress is now left alone for 3 minutes, dequeue
      reports it, a failed match insert releases it, and a waiting player's
      status poll claims an older waiting row. Domino gets the same status-side
      match. Raced 4/4 (chess) and 3/3 (domino) with simultaneous queue calls.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      c227bce1
    • Mahmoud Aglan's avatar
      fix(auth): the crown no longer clips on tall forms; game checkbox · 42dd98c4
      Mahmoud Aglan authored
      justify-content centred the register form past the top of the screen;
      auto margins centre it without overflowing. The age checkbox is a carved
      slot that pops a green tick, and checkboxes squash and click like buttons.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      42dd98c4
    • Mahmoud Aglan's avatar
      feat(challenges): sticker tiles, chunky gold bars, English titles · 1b5018a6
      Mahmoud Aglan authored
      Each challenge shows its game's sticker (ludo_played, chess_played...) and
      a ready one glows gold until collected. The all-done panel was never shown;
      it now appears once every challenge is claimed. The API's fallback icons
      and the stored templates use sticker names instead of emoji.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      1b5018a6
    • Mahmoud Aglan's avatar
      fix(ludo): two players who search at the same moment are matched · 3df90193
      Mahmoud Aglan authored
      Both missed each other in the queue call and both inserted; status never
      tried to match, so they waited for the 60 s timeout. A waiting player's
      status poll now claims fresh rows queued before its own, after locking its
      own row, so two pollers can never claim each other. ludo_queue stores the
      requested table size for that path.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      3df90193
    • Mahmoud Aglan's avatar
      fix(friends): the host can cancel a challenge it sent · 425202c0
      Mahmoud Aglan authored
      decline-invite only accepted the invited player, so a host backing out of the
      lobby got 403 and the friend could still accept into an empty game for two
      minutes. The host now cancels (result 'cancelled'); the target still declines.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      425202c0
    • Mahmoud Aglan's avatar
      67e39c8d
    • Mahmoud Aglan's avatar
      fix(tools): shoot.mjs deletes the matches and rows its account creates · 5ed5daf6
      Mahmoud Aglan authored
      Bot game scenes create domino/ludo/backgammon match rows and, now that payouts
      work, economy and rating rows; removeAccount left them behind. Twelve orphaned
      domino matches from earlier runs were deleted from production.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      5ed5daf6
    • Mahmoud Aglan's avatar
      feat(game): vibration switch in settings · 00e13552
      Mahmoud Aglan authored
      Haptics (feel.haptic) are off when the player turns vibration off; the
      setting persists with the rest of the player's preferences.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      00e13552
    • Mahmoud Aglan's avatar
      chore(design): detector exceptions for game motion; XP bar animates transform · 8822f88d
      Mahmoud Aglan authored
      bounce-easing is the owner's direction (El3ab is a game: pops and overshoot);
      side-tab flags the 3D bevel under buttons and cards. The XP bar used to
      transition width (layout thrash); it now scales.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      8822f88d
    • Mahmoud Aglan's avatar
    • Mahmoud Aglan's avatar
    • Mahmoud Aglan's avatar
      feat(game): announce new achievements the moment a game ends · 3e2a893c
      Mahmoud Aglan authored
      Achievements were only checked when the achievements screen was opened, so a
      new one was never celebrated. After every game the HUD now refreshes the player
      from the server (the purse counts up, a level-up fires if earned), runs the
      achievement check and drops a gold banner for each new one with its coin
      reward, the reward jingle and a haptic.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      3e2a893c
    • Mahmoud Aglan's avatar
      feat(game): a full-screen level-up celebration · 28883801
      Mahmoud Aglan authored
      When the server raises your level, the table dims and the crown slams in on
      spinning rays with the new level in a gold star, confetti, the level-up jingle
      and a heavy haptic. shoot.mjs --eval runs a snippet before the shot.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      28883801
    • Mahmoud Aglan's avatar
      fix(game): toasts sent over the bus were dropped; friend rows get game buttons · 8cb1f74b
      Mahmoud Aglan authored
      Nothing listened for bus.emit('toast'), so sync errors, login-required notes
      and group messages silently vanished. engine.js now shows them with a new
      ui.toast (parchment note with a sticker and a sound). Friend rows: Challenge is
      a green game button, chat a sticker button. shoot.mjs --with-friend makes a
      befriended second throwaway account.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      8cb1f74b
    • Mahmoud Aglan's avatar
      feat(game): a real loading screen; tournament tiebreak names · 6abd77c5
      Mahmoud Aglan authored
      - The splash is a game loading screen: the crown dealt onto spinning light
        rays, the wordmark, a chunky gold loading bar and a rotating tip.
      - index.php shows the crown and a loading bar before any script runs, so a slow
        phone never sees a blank page.
      - Tiebreak names (Buchholz, Sonneborn-Berger, ...) were raw keys on the
        tournament page.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      6abd77c5
    • Mahmoud Aglan's avatar
      fix(online): matchmaking for ludo, domino and backgammon actually matches · 1b6fe1dc
      Mahmoud Aglan authored
      Found by the gameplay pass, verified against production with throwaway
      accounts (31/31 checks):
      
      - ludo: the queue filtered and ordered on created_at, a column ludo_queue does
        not have, so no two players were ever matched; five stale rows from July to
        September were also deleted.
      - domino and ludo status/get decoded jsonb columns that already arrive as
        arrays: a PHP 8 TypeError, so the waiting player never learned of the match
        and every live poll failed.
      - all three: the zero-UUID claim placeholder was reported as a found match and
        its row deleted; failed inserts were reported as success; domino hands were
        renumbered by array_merge; complete passed p_winners as a string (the RPC
        refused it) and could pay twice. Complete now pays once, to seated players.
      - engine.js loads each game module independently, so one module failing to
        load can no longer stop the whole app from booting.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      1b6fe1dc
    • Mahmoud Aglan's avatar
      feat(game): rewards, shop, profile and settings feel like a game; coin payouts fixed · 6746a37a
      Mahmoud Aglan authored
      - Daily reward is a moment: a treasure chest on spinning light rays that
        wiggles when ready, a gold "open" button, the chest bursts with the reward
        jingle and coins fly to the purse; the week is a grid of reward cards with
        the chest on day seven.
      - Shop: loot cards framed and ribboned by rarity (grey, green, blue, purple,
        gold), drawn previews per item type (mini boards in their colours, piece
        sets, framed avatars, effects), coin and gem price tags; Arabic item names
        (api/shop.php now sends name_ar).
      - Achievements as sticker badges, grey with a lock until earned, gold when
        done; chunky gold progress bars. Leaderboard podium medals with ribbons.
        Settings: sticker tiles, a green game switch, the delete button is red, and
        a credits sheet for the CC BY artists. Profile: framed avatar, crown level,
        sticker stats and game marks; the Challenge button pushed a scene that does
        not exist and did nothing.
      - Signed out there is no HUD or tab bar; login sits in a framed tray with a
        big gold "play as guest" button.
      - HUD/tab crown uses the optimised crown.webp; wood and crown are preloaded.
      - Server: no game had paid coins or XP since 2026-06-18 because award_coins
        wrote a transaction type the table's check constraint rejects; fixed in the
        database (migrations/2026-10-19-award-coins-type.sql). Online backgammon had
        no tables at all; created (migrations/2026-10-19-backgammon-online.sql), and
        the backgammon friend invite now writes the same shape as matchmaking.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      6746a37a
    • Mahmoud Aglan's avatar
      feat(design): Royal Cafe, El3ab becomes a mobile game · bfb16a6e
      Mahmoud Aglan authored
      The owner's direction: El3ab is a game, not an app; the indigo pass read as a
      "big blue blob". The world moves onto a warm walnut cafe table under a lamp.
      
      - New game UI kit (tokens.css, core.css): leather trays for lists, ivory card
        stock in gold frames, chunky glossy 3D buttons (green play, blue, gold, red,
        purple, wood) that push down, ribbon screen titles, round back buttons, gold
        tab chips. Lalezar for display type, Baloo Bhaijaan 2 for reading.
      - Game HUD: avatar frame with level badge and XP bar (500 XP a level, as the
        server computes it), coin and gem purses with green plus buttons (reward,
        shop), wooden bell. Tab bar: sticker icons that lift when active, a big round
        play button with the crown standing out of the bar.
      - 60 glossy sticker icons baked from game-icons.net (CC BY 3.0, credited in
        public/assets/game/CREDITS.md) by tools/build-gicons.mjs; gicon()/crown()
        in icons.js; legacy emoji keys and icon slots now render stickers.
      - Illustrated SVG scene per game (core/game-art.js), used on the home hand and
        each game room.
      - Home is a game lobby: side buttons (reward chest that wiggles when ready,
        achievements, leaderboard, challenge) around the crown, the hand of game
        cards, a huge green Play online button, a live-tournament banner.
      - Rooms use shared game-mode buttons and a framed art hero (ui.js).
      - Sound: recorded card-table sounds (Kenney, CC0) through Web Audio with
        variants and pitch drift; synth fallback. Global press feel (feel.js),
        card-move scene transitions, counting purse, ringing bell.
      - Fix: the shop module was never loaded, so the shop could not open.
      - Restyled screens from the parallel pass (play flow, chess and puzzles, rank
        and tournaments, social, profile, shop, rewards, auth) and ~200 recovered or
        missing Arabic/English strings; t() now warns once per missing key.
      
      Ludo, backgammon and domino game screens and the shared in-game panels ship
      separately, after their gameplay pass.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      bfb16a6e
    • Mahmoud Aglan's avatar
      feat(design): play flow and game rooms in The Cafe Deck · c284b158
      Mahmoud Aglan authored
      - Chess bots are court cards: the bot's initial printed large in red or black
        ink by strength, name plate, a one-line personality, the rating band as the
        corner index; the strongest carries a crown.
      - Time controls are printed cards grouped by bullet, blitz, rapid, classical.
      - Matchmaking shows a face-down deck riffling while the clock counts.
      - Challenge-a-friend rows, a card-stock options sheet; the pre-game lobby shows
        you and your opponent as two player cards, the opponent face down until they
        accept. The lobby's clock chip now shows the real control; every classical
        control used to read 30 minutes.
      - Ludo, domino and backgammon rooms: chip groups printed on card stock when
        chosen, ludo seats in each player's ink, one red start button. Domino bot
        setup is one screen (level and target, then start) instead of two steps.
      - PRODUCT.md: El3ab is a game, not an app; binding for every screen.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      c284b158
    • Mahmoud Aglan's avatar
    • Mahmoud Aglan's avatar
    • Mahmoud Aglan's avatar
      feat(admin): branding edits The Cafe Deck tokens; theme saves need the admin · 034fc175
      Mahmoud Aglan authored
      - The colour section now edits the deck tokens (card back, card stock and
        inks, foil and signals, text) with tokens.css values as defaults; the top
        and tab bar section only lists values theme.js still applies.
      - Retired sections (home tiles, quick actions, game menu sheet, menu buttons,
        chips, bot cards, game gradients, pill radii) are gone; saving them did
        nothing after the redesign, and their old defaults could have overridden it.
      - Emoji replacement slots become icon overrides (icon_<name>); slots that are
        now drawn icons are dropped from the asset list. Board defaults match the
        printed boards.
      - POST /api/branding.php required no login at all: anyone could rewrite the
        theme for every player. It now requires the branding admin session.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      034fc175
    • Mahmoud Aglan's avatar
      feat(design): shared screen parts, emoji-free strings, printed chess board · 6b2507b8
      Mahmoud Aglan authored
      - core/ui.js: screenHead/wireBack, emptyState, loadingRows, errorState, avatar,
        esc, so every redesigned screen is built from the same furniture.
      - i18n: 44 strings carried emoji or check-mark glyphs; they now read as words.
      - Chess board dark squares move to a mid-tone printed indigo so both piece
        colours read on them.
      - tools/shoot.mjs can push any scene with params.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      6b2507b8
  2. 18 Oct, 2026 1 commit
    • Mahmoud Aglan's avatar
      feat(design): The Cafe Deck, foundation and home · 7119c142
      Mahmoud Aglan authored
      El3ab is redesigned as the deck on a cafe table: an indigo card-back ground
      printed with a fine lattice, white card-stock faces in black and red ink, gold
      foil kept for the crown, coins and first place. One face, Alexandria, for every
      role, with tabular figures so clocks and ratings never jitter.
      
      - tokens.css and core.css rewritten. The old token names still resolve, into
        the new palette, so every screen picks up the world before its own pass.
      - New icon family (public/js/core/icons.js): Lucide for utility icons plus
        El3ab's own crown, coin, gem, suits and four game marks. emoji() and the old
        PNG icon slots now render these, so the 300 emoji call sites stop printing
        emoji. Admins can still override any icon via an "icon_<name>" asset.
      - theme.js applies only the new keys; the retired palette's stored values can
        no longer repaint the app. getColor() falls back to the matching token.
      - HUD: avatar, name and level, coin and gem purses, bell. Tab bar marks the
        active world with a red diamond pip.
      - Home is now a fanned hand of the four games with your rating in each card's
        corner; the chosen card is pulled up and its lobby sits below it.
      - tools/shoot.mjs screenshots scenes as a throwaway signed-in player.
      - docs/branding-backup-2026-10-19.json: the stored theme before this change.
      Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
      7119c142
  3. 01 Sep, 2026 12 commits
    • Mahmoud Aglan's avatar
      fix(tournaments): a double no-show is no longer a free point for Black · aa75a0c8
      Mahmoud Aglan authored
      When neither player opened their board, the sweep called
      tournamentForfeitPairing with the white player's id as the absentee. That
      helper awards the point to whichever seat is NOT the one named, so every game
      nobody turned up for was recorded as black_wins — a full point, in a rated
      event, for a player who also did not appear. It fired on all four boards of a
      round during testing.
      
      An unnamed absentee now records the pairing as 'aborted', which
      chessResultPoints already scores 0-0, and the standings count it as a loss for
      both rather than falling through to the draw branch.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      aa75a0c8
    • Mahmoud Aglan's avatar
      fix(tournaments): let the manager drive the pairing engine · 62db8358
      Mahmoud Aglan authored
      api/cron.php now accepts the Supabase service key as an alternative
      credential, so the management app can run the engine without a second
      shared secret having to be provisioned on two CapRover apps first, and
      takes an optional tournament_id so an organiser pressing Start gets the
      answer now instead of at the next sweep.
      
      The sweep also stops skipping tournaments with no round. Flipping a
      tournament to in_progress without pairing round one left it registered,
      running and permanently unplayable; the sweep now opens the round.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      62db8358
    • Mahmoud Aglan's avatar
      fix: a tournament started with no round no longer deadlocks · 929263b6
      Mahmoud Aglan authored
      The manager's Start button flips el3ab_tournaments.status to in_progress
      directly. For a tournament this engine owns, nothing else was ever going to pair
      round 1 — so the tournament sat registered, started, and permanently unplayable,
      with current_round 0 and no round row.
      
      tournamentTick now opens the round when it finds that state, which also repairs
      any tournament already stuck in it. Covered by e2e section 21.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      929263b6
    • Mahmoud Aglan's avatar
      fix: ui-audit tears down its guest accounts even when the browser crashes · 76109e6f
      Mahmoud Aglan authored
      The audit signs in as a guest per device, which creates a real account. A single
      long-lived browser was crashing partway through the third device, and the crash
      took the teardown with it — leaving throwaway accounts in production.
      
      Now: one browser per device, and teardown in a finally block so it runs whichever
      way the run ends.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      76109e6f
    • Mahmoud Aglan's avatar
      fix: the chess board overflowed any padded container · 13cbc287
      Mahmoud Aglan authored
      ChessBoard sized itself from container.clientWidth, which INCLUDES padding, so
      inside a padded container the board came out exactly the padding too wide. On the
      puzzle screen that pushed the whole page 4px sideways and hung the board off the
      edge. Sizing now measures the container's content box.
      
      resize() had a second bug: it set canvas.width/height (the backing store) but
      never the CSS width/height, so after any resize the element laid out at its
      attribute width — devicePixelRatio times too large. It also now resets the
      transform before scaling rather than relying on the implicit reset.
      
      Both paths share one availableSize(), so they can no longer disagree.
      
      Local audit is clean across all scenes at 360/390/430px.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      13cbc287
    • Mahmoud Aglan's avatar
      fix: UI sizing and spacing that did not fit real phones · 9030409a
      Mahmoud Aglan authored
      An audit of the live app at 360/390/430px found 355 measurable layout defects.
      tools/ui-audit.mjs drives the real app at those three widths and reports what is
      measurable rather than a matter of taste: horizontal overflow, tap targets below
      44px, text below a legible size, and content clipped by its own container.
      
        before  355  (tiny-text 216, tap-target 133, clipped 6)
        after     0
      
      The root cause of the size problems was not the stylesheet. /api/branding.php
      writes 181 stored values inline onto :root at runtime, overriding every token in
      tokens.css — including quick_btn_label_font 10 and hud_badge_font 9. Editing
      tokens.css alone changed nothing. The floors are now enforced in theme.js where
      the override happens, so no stored value (or future admin edit) can reintroduce
      illegible text or an unreachable control:
      
      - any *_font token is floored at 11px
      - tap-target tokens are floored at 44px
      - home_max_width, stored as 340px for a 360px phone, is treated as a floor and
        allowed to grow (min(100%, max(340px, 92vw))) instead of pinning every phone
        to the smallest one's layout
      
      Alongside that: a fluid type scale (--fs-xs … --fs-xl) and --tap-min in
      tokens.css, a tap-target floor on every control in core.css, and 53 sub-11px
      inline font sizes and 41 undersized inline button heights raised at the source.
      
      Layout fixes visible on screen:
      - the daily-gift button carried breathe-glow on the whole flex column, drawing a
        large dark rectangle across the quick-actions row. The glow belongs on the icon.
      - play home dumped its spare height as dead space above the tab bar; it now
        centres via auto margins, which does not clip when content overflows the way
        justify-content:center does.
      - the chess board left a stray "1." floating in a gap above the controls. The
        opening/material/move-list block is now hidden until there is something in it.
      
      Also adds tools/dev-server.php, which serves the app locally against the real
      Supabase so this work does not need a deploy per iteration.
      
      Multiplayer regression: tests/run.sh 5/5, and the production rehearsal passes.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      9030409a
    • Mahmoud Aglan's avatar
      feat: one reactions system for every game, on its own channel · 09afc0f8
      Mahmoud Aglan authored
      Reactions did not sync, and when something appeared it floated in the middle of
      the screen with no sign of who sent it. Four games had four implementations,
      three broken in a different way:
      
      - chess kept ONE shared `emote` slot in the match's game_state, so two players
        reacting in the same second overwrote each other, and it carried no sender;
      - ludo posted reactions through the MOVE endpoint, which replaces game_state
        wholesale — sending an emote wiped turn_count and broke the sync loop. Its
        receive side registered with mp.onEmoteReceived, which only stores a callback
        that nothing ever invokes, so ludo players never saw a reaction at all;
      - domino had a third variant, again a single shared slot;
      - backgammon wrote a `last_emote` column no client ever read, so the opponent
        saw nothing. (That table does not exist in the database either — see below.)
      
      Reactions are chat, not game state. They now live in `chat_messages` on a
      `match` channel via the new api/match-chat.php, so nothing about a reaction can
      reach a board, a clock or a turn — a social feature must not be able to corrupt
      a game in progress. The multiplayer sync path is untouched.
      
      core/reactions.js is the single client implementation. Its central idea is
      SEATS: a game registers which element belongs to which player, and every bubble
      is then drawn attached to that player, carrying their name and avatar. That is
      the actual fix for "you can't tell who is talking". Seat resolution falls back
      to "the seat that isn't mine" in a two-player game, so a game can register its
      opponent seat before it knows the opponent's id.
      
      Only preset emotes and preset phrases are accepted, validated server-side —
      free text between strangers in a live match is a moderation surface nobody is
      staffing. The cooldown is enforced on the server, not just in the UI.
      
      Also fixed, found on the way:
      - ChessBoard has draw(), not render(). board.js called render() whenever a
        themed piece image finished loading, so it threw every time and the board
        never repainted with custom piece art.
      - chess/scenes/game.js built an inline onerror="" whose body embedded the result
        of emoji() — which returns <img src="..."> when a themed asset exists. Its
        double quotes closed the attribute, breaking the markup and throwing a
        SyntaxError on every bot game; the stray '"> rendered as text inside the
        avatar. The fallback is wired in JS now.
      
      Adds tools/ui-audit.mjs (drives the real app at three phone widths and reports
      measurable layout defects) and tools/test-reactions.mjs (end-to-end reaction
      check against a live deployment, with teardown).
      
      Noted, not fixed here: backgammon_matches and backgammon_queue do not exist in
      the database, so backgammon multiplayer has never worked. Out of scope for this
      change and untouched.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      09afc0f8
    • Mahmoud Aglan's avatar
      docs: handover reflects the deployed and rehearsed state · 28e6b267
      Mahmoud Aglan authored
      Deployed to el3ab-player and verified end to end against production with
      tests/rehearsal.mjs, which now also covers spectating and the "watch this player"
      lookup. Production is left byte-for-byte as it was found: row counts across
      matches, tournaments, profiles, rating_history and the queue are unchanged.
      
      Remaining for a human: play one game on a real device (the interface changes are
      the part I could not verify), and optionally set CRON_SECRET and
      SUPABASE_JWT_SECRET, which need CapRover app configuration the push webhook does
      not grant.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      28e6b267
    • Mahmoud Aglan's avatar
      fix: restore spectating, and make "watch this player" watch the right player · 2710cab1
      Mahmoud Aglan authored
      Two problems, one of them mine.
      
      Mine: locking api/game.php's `get` to the two participants broke spectating
      outright. Watching a live board is a normal part of a chess event — the profile
      "watch" button and the tournament live board both rely on it — and on
      championship day it is what most people will be doing. Non-participants now get a
      spectator projection: position, clocks, move list, players and result, but no
      `my_color` and none of the private game_state (draw offers, heartbeats).
      
      Pre-existing: handleFindActiveMatch ignored the player_id it was given and always
      looked up the *caller's* match, so clicking "watch" on someone else silently
      opened your own game. It now honours the requested player. The board itself still
      goes through `get`, which decides what a spectator may see.
      
      Both lookups are also bounded to the last 12 hours. Production carries 27 matches
      stuck in a non-final state since May and July; without that bound, "watch" would
      open a game that ended three months ago.
      
      Test-harness hardening in the same commit: pgshim silently ignored any PostgREST
      filter it did not implement, so a query with `gte.` matched every row and the test
      passed while production filtered correctly. It now implements gte/lte and throws
      on anything unrecognised, rather than quietly matching everything.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      2710cab1
    • Mahmoud Aglan's avatar
      fix: creating a tournament without a start time failed against production · fcc74b9f
      Mahmoud Aglan authored
      el3ab_tournaments.starts_at is NOT NULL in the live schema. tournament-admin.php
      allowed it to be null, so every create call that did not supply one was rejected
      by Postgres. The local test schema had the column nullable, so the suite passed
      and the defect only appeared when the rehearsal ran against production.
      
      starts_at now defaults to now() — which also gives auto_start something to act on —
      and tests/schema.sql marks the column NOT NULL so the next one is caught locally.
      
      Adds tests/rehearsal.mjs, the script that found it: four throwaway accounts driven
      through matchmaking, a 33-move game to checkmate and a full Swiss round against the
      live deployment, with teardown in a finally block. Run it after any deploy.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      fcc74b9f
    • Mahmoud Aglan's avatar
    • Mahmoud Aglan's avatar
      fix: a bot outage no longer freezes the board on "thinking…" · 231ba42e
      Mahmoud Aglan authored
      requestBotMove retried forever on failure and did nothing at all when the engine
      answered without a move, so a Stockfish hiccup left the player watching the
      thinking indicator with no way to continue.
      
      Now: four attempts with exponential backoff, then a dialog offering to end the
      game. Also guards against gameState having been torn down mid-request.
      
      The engine itself is healthy — verified live: GET /api/chess/bots returns the
      seven bots the client expects, and POST /api/chess/move answers the start
      position with d2d4 at depth 3. api/bots.php's paths are correct.
      
      Arabic and English strings added for the new message.
      Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
      231ba42e