- 26 Oct, 2026 5 commits
-
-
Mahmoud Aglan authored
org-applications: approve and reject are one call each to decide_application() (El3ab docs/redeem-contract.md 7.5), which adds or upgrades the membership, runs any grants a code application was holding and records the staff reviewer in reviewed_by_admin, all in one transaction. The player is notified only after it answers ok, with the org's name from that answer in title_ar/body_ar and the reviewer's note or reason in the body. The old code wrote missing columns and an admin id into a profiles key, so nothing was ever approved, and then told the player they were in. Bulk approve and reject go through the same path (up to 50) and say how many failed and why. The list and show pages read el3ab_organizations (the org column was blank for all 13), with per-status counts and the cancelled status. The proof is shown through a ten-minute signed link, from the private org-proofs bucket when the file is there (core/ProofDocument.php), and a proof still sitting in the old public bucket is flagged. Signed links now carry the apikey Kong requires. Org scoping: core/OrgScope.php, called by the Router before every route, keeps an org-scoped staff account (admin_users.org_id) inside its own org: the org in organizations/{id}/... and {orgId}, the owner org of any record named in the path (application, code, event, channel, frame, transfer, ...), and org ids in the query or form are all checked with Auth::canAccessOrg; lists are pinned to the account's org, the org directory goes straight to it, and modules the role has no permission for at all are refused. OrgPermissions uses player_id and status='active', ranks moderator with arbiter, and treats an org-scoped staff account as the owner of its own org only, as the database functions do. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Categories, manual pairing, unpairing and TRF/crosstable export belong to the external Swiss service; for a tournament the app runs they now point to the arbiter tools and the full JSON export instead of 'no Swiss link' or 'round not found'. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
The game reads all five tables and the manager had no screen for any of them; the only battle-pass season ended on 2026-07-01, so the pass has been dead since. One small config-driven CRUD covers them, every field a real column, with Arabic labels and a note on what the game actually uses: - seasons: create the next one (Cairo times), and "activate", which makes it the single is_active row the game's battlepass.php reads (an ended season cannot be activated, the active one cannot be deleted); - shop items (cosmetics) with their enum types and rarities; - achievements, with the condition written as {type, count} and the condition types the game never counts marked as such; - daily challenge templates, marking the types the game does not count; - puzzles, validating the FEN and the UCI solution. Deleting something players own or have earned is refused by the foreign key and says so instead of failing silently. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Campaigns saved as 'scheduled' were never looked at again: nothing in either app processed them. CampaignService::processDue() sends every due campaign into the game's bell (notifications with title_ar/body_ar, as the game's notifications.php reads them) and records recipients, claiming each one with a conditional scheduled -> sending update so overlapping runs never send twice. It runs from GET/POST /api/cron/notifications (X-Service-Key, or X-Cron-Secret / ?secret= when CRON_SECRET is set, the same credentials as the game's api/cron.php) for a scheduler, and as a fallback at most once a minute after any staff page load. Scheduling now stores the typed Cairo time as UTC (the raw datetime-local value was saved as UTC, 2-3 hours late) and refuses past times; filters are stored as jsonb, not a JSON string; cancelling only cancels a campaign that has not started; send-now shares the same delivery code. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Both apps keep their theme in platform_theme, but the game only applies the keys in its theme.js COLOR_KEYS (back_*, card, ink, gold, board_*, ludo_*, bg_* ...). The palette this page edited (color_primary, surface_* ...) is the manager's own theme, so no change here ever reached a player. A new "app colours" tab edits exactly the game's keys, in groups with Arabic names, showing each one's default read from the game's tokens.css; reset removes the override so the app falls back to its design. The manager palette stays, labelled as the manager's. Every asset slot is marked as read by the game, read only for uploads made after 2026-10-19 (logo, tab icons), or ignored (the old emoji slots and the game-icon slots the app now draws itself), and new uploads may create any slot the game reads (icon_*, chess_piece_*, chess_bot_*, emoji_dice_1..6). Uploads go through SupabaseStorage: the hand-written request lacked the apikey header and Kong refused every upload. Flash messages are JSON-encoded into the toast script; a multi-line message (such as a storage error) used to break the script. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
- 25 Oct, 2026 9 commits
-
-
Mahmoud Aglan authored
The settings page edited system_config, which nothing in the game reads. It now edits reward_config: coins and XP per game and place (read by complete_match() and domino-match), the seven-day daily reward ladder and the chess Elo K-factor steps (El3ab includes/game-config.php). All 47 keys have an Arabic label and group and say whether the game reads them today; chess draw/loss and backgammon loss rows are marked unpaid because those games report only the winner to complete_match(). Values are validated whole numbers stored as plain text (complete_match casts value::int); only changed rows are written and the change is audited. system_config is shown read-only for reference, with where each value really comes from. Its update no longer json_encode()s the value before PostgREST encodes it again (the cause of elo_k_factor_master = "\"20\"", repaired in the migration). Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Iterating a list of objects as a place => amount map passed an array to htmlspecialchars() and broke the public page; places were also marked with emoji. Both shapes are read now, places are medal icons. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
The game team is landing registration-window, rating-limit and prize-payout enforcement; hints that said those were not enforced would go stale the moment it deploys. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
feature_flags has id, name, description, is_enabled, rollout_percentage and conditions. The list ordered by a category column that does not exist, so the query failed and the page always said there were no flags (13 exist); create and edit wrote label, label_ar, target, category and target_value and were all rejected. Flags now get Arabic names and groups for the platform's own ids, a partial rollout is rollout_percentage, and the maintenance_mode message, English message and end time go in conditions ({message_ar, message, until}) as the game's api/config.php reads them. Turning maintenance on asks for confirmation; built-in flags cannot be deleted, only switched off. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Every one of these was rejected by PostgREST and reported as success (found by checking each insert/update/select against the live schema): - workflows: approval_requests has reviewed_by/review_notes, not reviewer_id/review_note; reviewers and requesters are staff uuids, so the pages now show their names, "my requests" filters by the account id, and search no longer runs ilike on a uuid. workflow_rules has event_type, not event; created_by is a uuid; conditions/actions go in as jsonb lists. - RuleEngine: event_type, and its notify action writes a real notifications row (user_id, title_ar/body_ar) only when addressed to a player. - moderation: cheat_reports.reviewed_by references profiles, so a staff reviewer is stored in the new reviewed_by_admin column. - users: admin_users has no created_by. - org-transfers, org-frames, org-chat: org_members has player_id (not user_id) and no status column. Membership logic is unchanged. - org-achievements: an org achievement cannot be stored in player_achievements (its achievement_id references the game's achievements); granting now says so instead of failing the insert and still paying the reward. Progress reads completed/completed_at. - org-loyalty, org-partnerships: those tables have no updated_at. - live pages: phases are in tournament_phases ordered by phase_number; the arena tables never existed. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Grant and revoke (player page and economy module) used to read the balance, add in PHP, write it back and insert the ledger row separately, racing any game payout in between. They now call admin_adjust_currency() (one statement, coins or gems, never below zero, booked as admin_grant / debit; see El3ab migrations/2026-10-26-manager-columns.sql) and put a notice in the player's bell (title_ar/body_ar, as the game's notifications.php reads them). award_coins() was not used: coins only, no floor, and it books staff grants as game income. Edit no longer sends profiles.email (no such column; PGRST204 rejected the whole update); the login email is shown read-only from the auth account. Create now makes a real login account through GoTrue's admin API, whose trigger creates the profile; the old insert had no id and always failed. The match history query used a players column matches does not have, and the transactions tab read a variable that was never set. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Populate used to attach a new Swiss-service tournament to whatever it was pointed at, which switched a game-run tournament to the other engine, and registered bots with no profile that real players were then paired against. Population is refused once a real player has registered or the tournament has left draft, and simulation while any real player is entered, unless a superadmin ticks an explicit test override. Game-run test tournaments are simulated natively: start through the game, then results drawn from the bots' ratings are recorded through the game's arbiter endpoint and the engine pairs the next round itself. bot_metadata is stored as jsonb, not a JSON string. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Create and edit write real el3ab_tournaments columns. Edit used to send organization_id, rounds_count, entry_fee, prize_pool, start_date and end_date (none exist) and complete sent completed_at, so PostgREST rejected both and the page said saved. The form now also covers name_ar, min_players, min/max rating, is_rated, auto_start, the registration window and a real prize distribution ([{place, coins}]), validates them, and reads and writes times in Cairo (stored as UTC, DST included). Editing is allowed while registration is open; the edit page no longer crashes on phase_config. No more Swiss linking on create: picking an organisation used to create a Swiss-service tournament and hand the event to a second engine. Tournaments the player app runs (no swiss_api_tournament_id) are shown from el3ab_tournament_rounds.pairings through NativeTournamentService: every board with names, results, reasons and who set them; standings with the engine's own scoring (aborted boards are losses, per-time-control rating) and the chosen tiebreak columns; the players tab resolves names from profiles (there is no player_name column) including withdrawn players. Arbiter tools relay to the game's api/tournament-admin.php with the service key: set or override a result, forfeit (either side or both), next round, close the round now (unfinished boards 0-0), pause/resume, end early with final placings, add a player by username (late entry once running), withdraw, bulk add by usernames. Start goes through the game so the minimum-players check is no longer skipped. Tiebreaks save directly for these tournaments and offer only the four keys the engine computes, under the names the app shows players. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Database used to decode the PostgREST error body and hand it back as if it were the written row, so writes to missing columns (PGRST204), bad types and broken foreign keys came back truthy and the controller flashed success. Every failed request is now recorded and logged; insert/update/rpc return null on failure; Response::success, redirect-with-success and json refuse to report success over a recorded write failure and show the database's reason. Best-effort writes (audit trail, page-view counters, last login) go through Database::soft() so they cannot fail the action they describe. AuditLog had never written a row: staff ids broke the actor_id foreign key to profiles, names broke the resource_id uuid, and details was double encoded. Staff now go in admin_user_id and text ids in resource_ref (El3ab migrations/2026-10-26-manager-columns.sql); the audit page shows the admin's name and searches the text reference. Staff type times in Cairo: the app now runs in Africa/Cairo and core/Time converts typed times to UTC instants and back, DST included. Co-Authored-By:Claude Opus 5.5 <noreply@anthropic.com>
-
- 01 Sep, 2026 3 commits
-
-
Mahmoud Aglan authored
Covers hosting, announcing and monetising the format end to end, verified against the live code and database rather than inferred from column names. The headline finding is that the built-in multi-phase path — the preset literally labelled سويسري ← إقصاء — cannot work: startSwissPhase reads a swiss_event_id column that does not exist, startPhase merges that error into a success envelope so it fails silently, getQualifiedPlayers can only read the external Swiss service, and tournamentFinish completes the tournament before the advance buttons can be pressed. tournament_phases, tournament_brackets and bracket_matches all hold zero rows in production; it has never run. The document gives the path that does work — the Swiss natively, a manual cut, and the knockout as a second tournament whose phase 1 is the bracket, which is reachable because getPhasePlayers reads registrations directly for phase 1 — and is blunt about entry fees and prizes being displayed but never moved. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The external Swiss service returns pairings carrying whitePlayerId and blackPlayerId and no names at all, so every board on a Swiss tournament rendered "لاعب" against "لاعب" and the rounds timeline showed a column of question marks. Its standings response does carry names, so the two are joined in LiveDataService and both engines' pairings are normalised to one shape before any view sees them — which is also where the array-into-string fatals on this page came from. Its stats were hardcoded zeros: a finished five-round, 64-player championship reported that no game had ever been played, and the vitals section fell through to "nothing has been played yet". The counts are now derived from the rounds. Tiebreaks arrive as an ordered tiebreakValues list rather than named columns; read either, and drop the column entirely when a tournament has none rather than printing a column of em-dashes. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The manager could only ever see the external Swiss service. Every tournament the player app's own engine ran was invisible here: standings came back empty, pairings were read from a column that does not exist (swiss_api_round_id, against a swiss_round_id column), and three of the fallback tables the service queried have never existed in this schema. The public live page was also fatal. The Swiss service wraps its rows in {"data": [...]}; the standings view iterated the wrapper, so "data" + 1 crashed PHP 8 and printed a stack trace with server paths to anyone holding the link. Two more views did the same thing to the native pairing shape, which puts an object where the Swiss shape puts a name. - NativeTournamentService reads the engine's rounds and computes standings, pairings and tiebreaks from them; LiveDataService is native-first throughout. - display_errors is off in the image, so a fatal is logged, never rendered. - Start no longer flips a tournament to in_progress and leaves it unpaired, and Generate Round no longer answers "no link to the Swiss system" for every tournament EL3AB runs. Both ask the player app's engine, authenticating with the service key both apps already hold. Two controls on the live page were scenery. The player-compare selects were filled from a class the players view no longer emits, and choosing two names ran a function whose whole body hid the result; the player card modal needed a data-player payload nothing rendered. Both now work off data already on the page. Visually the page was rebuilt around what a spectator opens it for: the boards and the leaderboard first (the share box and compare tool used to be pulled above them on a phone by order:-1), a podium, avatars and form dots, board cards that show at a glance which games are still running, a readable rounds timeline, a knockout bracket with connectors, and a QR that leads the share card instead of hiding behind a disclosure. Sections no longer render at opacity 0 when script does not run. tools/verify-cycle.mjs runs the whole cycle across both apps and checks the manager reports what the players did; it passes. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
- 03 Jul, 2026 2 commits
-
-
Mahmoud Aglan authored
- .htaccess: allow fonts.googleapis.com in style-src and fonts.gstatic.com in font-src for IBM Plex Sans Arabic - Add empty org-applications.css/js so module assets don't 404 Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Adds a new tournament workflow step between draft and start: - draft → "فتح التسجيل" → registration (players can join) - registration → "بدء البطولة" → in_progress New route: POST /tournaments/{id}/open-registration Sets status from 'draft' to 'registration' with audit log. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
- 22 Jun, 2026 1 commit
-
-
Mahmoud Aglan authored
Local uploads were unreachable by the player app on a different domain. Now uploads go to profile-images/branding/ bucket with upsert, producing a full public URL that the player app can load directly. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
- 21 Jun, 2026 6 commits
-
-
Mahmoud Aglan authored
- last_seen → last_seen_at (explains why stats showed 0) - country → country_code - Updated searchPlayers select and individual view JS Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
The notifications.data column is JSONB. Database::insert() already json_encodes the full payload, so passing json_encode() double-encodes it into a string. Mobile app expects a Map, not a string. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- getTargetPlayers now uses select=* (matches working players module) - Removed moduleJS for compose/individual views (JS is inline) - These views were referencing non-existent JS file paths causing 404s Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
csrfField() doesn't exist — the actual pattern is a hidden input with name="_csrf" and value from Auth::csrfToken(). Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- Moved level, coins, and created_at to PostgREST `and` operator so min+max ranges work together instead of overwriting each other - Batch-insert campaign recipients (100/batch) instead of N HTTP calls Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Adds campaign management, demographic targeting, individual player notifications, reusable templates, scheduling, and campaign history. All notifications delivered via Supabase Realtime (insert into notifications table). Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
- 06 Jun, 2026 3 commits
-
-
Mahmoud Aglan authored
Kong requires the apikey query param for public object access via img tags. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Kong gateway requires the apikey header alongside Authorization. Without it, storage uploads fail with "No API key found in request". Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
- Form now supports file upload (enctype multipart/form-data) - Accepts PNG, JPG, WebP, GIF up to 10MB - Uploads to Supabase Storage (assets bucket, ads/ prefix) - Shows current image preview in edit mode - Falls back to URL field if no file uploaded - List view shows image thumbnails - Added splash and between_games ad slots Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
- 05 Jun, 2026 1 commit
-
-
Mahmoud Aglan authored
Supports the player app's in-game ad banner system. New columns added to ad_campaigns table via ALTER TABLE. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
- 31 May, 2026 1 commit
-
-
Mahmoud Aglan authored
The isStalemate() called the Stockfish API on every move just to check if the opponent had legal moves — doubling API calls from 80 to 160 per game. Now relies on the API returning no best_move as game-over signal. Differentiates checkmate vs stalemate by eval magnitude. Also reduced max moves from 80 to 60 — bot games at these levels should resolve well before that. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
- 30 May, 2026 9 commits
-
-
Mahmoud Aglan authored
Part A - Bot Game Engine (BotGamePlayerService): - Maps player ratings to 7 bot personalities (amina→grandmaster) - Plays actual chess games move-by-move via Stockfish API - Full FEN tracking, UCI→SAN conversion, PGN generation - Game termination: checkmate, stalemate, 50-move, repetition, 80-move adjudication - Stores complete games in existing `matches` table (pgn, moves JSONB, fen) Part B - Game Viewer: - Interactive chessboard (CSS grid + Unicode pieces) - Move-by-move navigation (first/prev/play/next/last + keyboard arrows) - Move list with click-to-jump - Last move highlighting - Auto-play mode (800ms per move) - Uses chess.js for position replay from UCI moves Integration: - "لعب مباريات حقيقية" checkbox in bot testing panel - "♟ عرض" (View Game) button next to each completed pairing with match_id - Route: /tournaments/{id}/game/{matchId} Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
1. Rounds tab now shows White | Result | Black columns with proper result badges (1-0, 0-1, ½-½) instead of raw enum strings. 2. BotSimulationService stores whiteName/blackName in results JSONB. 3. Fixed live-tournaments admin-controller and LiveDataService json_decode crashes on already-decoded JSONB arrays. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
tournament_registrations.player_id had FK to profiles(id) which rejected bot player inserts since bot UUIDs aren't real profiles. Dropped the constraint so bot testing works. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
1. pairings/results JSONB columns already decoded by Supabase — added is_array check before json_decode (same pattern as tiebreak fix). 2. Bot player_id was 'bot_xyz' string but column is UUID type — PostgREST silently rejected every insert. Now generates proper v4 UUIDs. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Controller was passing the raw response body (which has data + roundNumber keys) directly to the view. The foreach iterated over the wrapper object instead of the standings array. Also fixed field name mappings: Swiss API uses 'name' not 'player_name', 'gamesPlayed' not 'games_played', 'tiebreakValues' array not 'tiebreak'. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
If the Swiss tournament was already started (e.g. from a previous attempt), don't fail — just update local status and proceed with round generation. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
The API rejects bodiless POSTs without Content-Type (415 Unsupported Media Type) AND rejects Content-Type with literally no body (Body cannot be empty). Solution: always send Content-Type, and for POST/PATCH/PUT with null body, send empty object {} as body. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
1. Content-Type: application/json only sent when body is not null. Swiss API rejects bodiless POST/GET with that header. 2. Controller start() now calls Swiss API /start endpoint. 3. simulateNextRound() auto-starts Swiss tournament if local status is still draft/registration. 4. generateRound response parsing fixed — data is in {round:{id,...}}. 5. getPairings response wrapped in {data:[...]} — now handled. 6. Pairing field names use camelCase (whitePlayerId not white_player_id). 7. Player ratings fetched from listPlayers instead of expecting them embedded in pairing objects. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-
Mahmoud Aglan authored
1. ApiProxy was sending Content-Type: application/json on GET requests which the Swiss API rejects ("Body cannot be empty when content-type is set to application/json"). Now only sends Content-Type for POST/PATCH/PUT methods. 2. Players tab now reads bot name/rating from bot_metadata JSONB instead of non-existent player_name/rating columns. Co-Authored-By:Claude Opus 4.6 <noreply@anthropic.com>
-