# api_server.py from flask import Flask, request, jsonify import requests import os import json import config from urllib.parse import quote from datetime import datetime app = Flask(__name__) # Tier ranking system (higher number = better tier) TIER_RANK = { "HT1": 9, "HT2": 8, "HT3": 7, "HT4": 6, "LT1": 5, "LT2": 4, "LT3": 3, "LT4": 2, "-": 1, } def get_tier_rank(tier): """Get numerical rank for a tier (higher = better)""" return TIER_RANK.get(tier, 0) # Backup directory BACKUP_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "misc")) # Global log file log_file = None def init_logging(): global log_file os.makedirs(BACKUP_DIR, exist_ok=True) log_path = os.path.join(BACKUP_DIR, "api_debug.log") log_file = open(log_path, "a", buffering=1) # Get service role key from config or environment variable SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", config.SUPABASE_SERVICE_ROLE_KEY) SUPABASE_URL = config.VITE_SUPABASE_URL # https://liorcawnmqrbbjtwyeyh.supabase.co # Headers for Supabase API requests def get_headers(): return { "apikey": SUPABASE_SERVICE_ROLE_KEY, "Authorization": f"Bearer {SUPABASE_SERVICE_ROLE_KEY}", "Content-Type": "application/json", } @app.route("/upload", methods=["POST"]) def upload_player_data(): """API endpoint to upload/update player tier data""" try: if not SUPABASE_SERVICE_ROLE_KEY or "PASTE_YOUR_SERVICE_ROLE_KEY" in SUPABASE_SERVICE_ROLE_KEY: return jsonify({ "success": False, "error": "SUPABASE_SERVICE_ROLE_KEY not configured. Add your service role key to config.py" }), 500 data = request.json print(f"[API] upload_player_data received: {data}") # Validate required fields if not data.get("username") or not data.get("region") or not data.get("gamemode") or not data.get("tier"): print(f"[API] Missing required fields") return jsonify({ "success": False, "error": "username, region, gamemode, and tier are required" }), 400 original_username = data["username"] # Keep original case username_lower = original_username.lower() # For comparison region = data["region"] gamemode = data["gamemode"] tier = data["tier"] # Validate gamemode valid_gamemodes = ["vanilla", "smp", "pot", "sword", "uhc", "nethpot", "mace", "axe"] if gamemode not in valid_gamemodes: print(f"[API] [ERROR] Invalid gamemode: {gamemode}") return jsonify({ "success": False, "error": f"Invalid gamemode '{gamemode}'. Valid: {', '.join(valid_gamemodes)}" }), 400 # Step 1: Fetch existing player by checking all users and matching case-insensitively url = f"{SUPABASE_URL}/rest/v1/players" headers = get_headers() # Fetch all players to find case-insensitive match fetch_url = f"{url}?select=*" print(f"[API] Fetching all players for case-insensitive match") fetch_response = requests.get(fetch_url, headers=headers) all_players = fetch_response.json() if fetch_response.status_code == 200 else [] existing_data = [p for p in all_players if p.get("username", "").lower() == username_lower] print(f"[API] Found {len(existing_data)} matching player(s) (case-insensitive)") # Step 2: Build update data with tier + region tier_column = f"tier_{gamemode}" if existing_data and len(existing_data) > 0: # Player exists - use original stored username, update region and tier existing_player = existing_data[0] player_id = existing_player["id"] stored_username = existing_player.get("username") # Use the original stored case old_region = existing_player.get("region") update_payload = { "region": region, tier_column: tier } print(f"[API] Updating existing player ID: {player_id}") print(f"[API] Stored username: {stored_username}") print(f"[API] Region: {old_region} -> {region}") print(f"[API] Tier column: {tier_column} = {tier}") print(f"[API] Payload: {update_payload}") # Use PATCH to update region and tier column patch_url = f"{url}?id=eq.{player_id}" print(f"[API] PATCH URL: {patch_url}") response = requests.patch(patch_url, json=update_payload, headers=headers) print(f"[API] PATCH response status: {response.status_code}") print(f"[API] PATCH response body: {response.text[:500]}") if response.status_code in [200, 204]: print(f"[API] [OK] Successfully updated player {player_id}") return jsonify({ "success": True, "message": f"Player {stored_username} updated (Region: {region}, {gamemode} tier: {tier})" }), 200 else: print(f"[API] [ERROR] Patch error - Status {response.status_code}: {response.text}") return jsonify({ "success": False, "error": f"Supabase error: {response.status_code}", "details": response.text[:500] if response.text else "No response body" }), response.status_code else: # New player - create entry with original case username create_payload = { "username": original_username, # Store original case "region": region, tier_column: tier, } print(f"[API] Creating new player: {original_username}") print(f"[API] Tier column: {tier_column} = {tier}") print(f"[API] Payload: {create_payload}") # Use POST to create new player response = requests.post(url, json=create_payload, headers=headers) print(f"[API] POST response status: {response.status_code}") print(f"[API] Response body: {response.text[:200]}") if response.status_code in [200, 201]: print(f"[API] [OK] Successfully created new player {original_username}") return jsonify({ "success": True, "message": f"Player {original_username} created (Region: {region}, {gamemode} tier: {tier})" }), 200 else: print(f"[API] [ERROR] POST error - Status {response.status_code}: {response.text}") return jsonify({ "success": False, "error": f"Supabase error: {response.status_code}", "details": response.text[:500] if response.text else "No response body" }), response.status_code except Exception as e: print(f"[API] [ERROR] upload_player_data error: {str(e)}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e), "type": type(e).__name__}), 500 @app.route("/health", methods=["GET"]) def health(): """Health check endpoint""" key_status = "[OK] Loaded" if SUPABASE_SERVICE_ROLE_KEY and len(SUPABASE_SERVICE_ROLE_KEY) > 50 else "[ERROR] Too short or missing" return jsonify({ "status": "ok", "supabase_url": SUPABASE_URL, "service_role_key_status": key_status, "key_length": len(SUPABASE_SERVICE_ROLE_KEY) if SUPABASE_SERVICE_ROLE_KEY else 0 }), 200 @app.route("/delete_gamemode", methods=["POST"]) def delete_gamemode(): """Delete a specific gamemode tier for a player""" try: data = request.json print(f"[API] delete_gamemode received: {data}") if not data.get("username") or not data.get("region") or not data.get("gamemode"): print(f"[API] Missing required fields") return jsonify({ "success": False, "error": "username, region, and gamemode are required" }), 400 username_lower = data["username"].lower() # For comparison gamemode = data["gamemode"] tier_column = f"tier_{gamemode}" url = f"{SUPABASE_URL}/rest/v1/players" headers = get_headers() # Fetch all players and find case-insensitive match fetch_url = f"{url}?select=id,username" fetch_response = requests.get(fetch_url, headers=headers) all_players = fetch_response.json() if fetch_response.status_code == 200 else [] matching_players = [p for p in all_players if p.get("username", "").lower() == username_lower] if not matching_players: print(f"[API] Player not found") return jsonify({ "success": False, "error": "Player not found" }), 404 player_id = matching_players[0]["id"] stored_username = matching_players[0]["username"] print(f"[API] Found player ID: {player_id}, username: {stored_username}") # Reset the gamemode tier to '-' (enum default) update_payload = {tier_column: "-"} # Use PATCH to update the tier column patch_url = f"{url}?id=eq.{player_id}" response = requests.patch(patch_url, json=update_payload, headers=headers) print(f"[API] Patch response status: {response.status_code}") if response.status_code in [200, 204]: return jsonify({ "success": True, "message": f"Gamemode {gamemode} tier removed for {stored_username}" }), 200 else: print(f"[API] Patch error response: {response.text}") return jsonify({ "success": False, "error": f"Supabase error: {response.status_code}", "details": response.text[:500] if response.text else "No response body" }), response.status_code except Exception as e: print(f"[API] delete_gamemode error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @app.route("/delete_player", methods=["POST"]) def delete_player(): """Delete all data for a player""" try: data = request.json print(f"[API] delete_player received: {data}") if not data.get("username") or not data.get("region"): print(f"[API] Missing required fields") return jsonify({ "success": False, "error": "username and region are required" }), 400 username_lower = data["username"].lower() # For comparison url = f"{SUPABASE_URL}/rest/v1/players" headers = get_headers() # Fetch all players and find case-insensitive match fetch_url = f"{url}?select=id,username" fetch_response = requests.get(fetch_url, headers=headers) all_players = fetch_response.json() if fetch_response.status_code == 200 else [] matching_players = [p for p in all_players if p.get("username", "").lower() == username_lower] if not matching_players: return jsonify({ "success": False, "error": "Player not found" }), 404 player_id = matching_players[0]["id"] stored_username = matching_players[0]["username"] # Delete the player record by ID delete_url = f"{url}?id=eq.{player_id}" response = requests.delete(delete_url, headers=headers) print(f"[API] Delete response status: {response.status_code}") if response.status_code in [200, 204]: return jsonify({ "success": True, "message": f"Player {stored_username} deleted" }), 200 else: return jsonify({ "success": False, "error": f"Supabase error: {response.status_code}" }), response.status_code except Exception as e: print(f"[API] delete_player error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @app.route("/clear_all", methods=["POST"]) def clear_all(): """Clear all player data from database and backup to JSON""" try: print(f"[API] clear_all called") url = f"{SUPABASE_URL}/rest/v1/players" headers = get_headers() # Step 1: Fetch all data before deletion (for backup) fetch_url = f"{url}?select=*" fetch_response = requests.get(fetch_url, headers=headers) all_data = fetch_response.json() if fetch_response.status_code == 200 else [] print(f"[API] Fetched {len(all_data)} records for backup") # Step 2: Save to backup JSON file with timestamp if all_data: os.makedirs(BACKUP_DIR, exist_ok=True) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") backup_filename = f"backup_{timestamp}.json" backup_filepath = os.path.join(BACKUP_DIR, backup_filename) with open(backup_filepath, "w") as f: json.dump(all_data, f, indent=2) print(f"[API] Backup saved to {backup_filepath}") # Step 3: Delete all records headers["Prefer"] = "return=minimal" # Don't return deleted rows delete_url = f"{url}?id=not.is.null" response = requests.delete(delete_url, headers=headers) print(f"[API] Delete response status: {response.status_code}") if response.status_code in [200, 204]: backup_file = f"backup_{timestamp}.json" if all_data else "No data to backup" return jsonify({ "success": True, "message": "All player data cleared successfully", "backup_file": backup_file, "records_deleted": len(all_data) }), 200 else: print(f"[API] Delete error response: {response.text}") return jsonify({ "success": False, "error": f"Supabase error: {response.status_code}", "details": response.text[:500] if response.text else "No response body" }), response.status_code except Exception as e: print(f"[API] clear_all error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @app.route("/restore", methods=["POST"]) def restore_data(): """Restore player data with one of three merge strategies""" try: data = request.json print(f"[API] restore called with strategy: {data.get('strategy')}") backup_file = data.get("backup_file") strategy = data.get("strategy") # merge, overwrite, or clear_new if not backup_file or not strategy: return jsonify({ "success": False, "error": "backup_file and strategy are required" }), 400 if strategy not in ["merge", "overwrite", "clear_new"]: return jsonify({ "success": False, "error": "strategy must be 'merge', 'overwrite', or 'clear_new'" }), 400 # Load backup data try: backup_filepath = os.path.join(BACKUP_DIR, backup_file) with open(backup_filepath, "r") as f: backup_data = json.load(f) print(f"[API] Loaded {len(backup_data)} records from backup") except FileNotFoundError: return jsonify({ "success": False, "error": f"Backup file not found: {backup_file}" }), 404 url = f"{SUPABASE_URL}/rest/v1/players" headers = get_headers() # Fetch current data in database fetch_url = f"{url}?select=*" fetch_response = requests.get(fetch_url, headers=headers) current_data = fetch_response.json() if fetch_response.status_code == 200 else [] print(f"[API] Current database has {len(current_data)} records") if strategy == "merge": # Merge: Keep higher tiers from both old and new merged_data = merge_strategy(backup_data, current_data) elif strategy == "overwrite": # Overwrite: Keep old as priority, fill gaps from new merged_data = overwrite_strategy(backup_data, current_data) elif strategy == "clear_new": # Clear new: Keep only backup data, mark as restored merged_data = backup_data # Clear database and re-upload merged data delete_url = f"{url}?id=not.is.null" delete_headers = get_headers() delete_headers["Prefer"] = "return=minimal" requests.delete(delete_url, headers=delete_headers) # Upload merged data uploaded_count = 0 for record in merged_data: # Remove id field if it exists (let database generate new ones) record_copy = {k: v for k, v in record.items() if k != "id"} response = requests.post(url, json=record_copy, headers=headers) if response.status_code in [200, 201]: uploaded_count += 1 print(f"[API] Restored {uploaded_count} records using '{strategy}' strategy") return jsonify({ "success": True, "message": f"Data restored successfully using '{strategy}' strategy", "records_restored": uploaded_count }), 200 except Exception as e: print(f"[API] restore error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 def merge_strategy(old_data, new_data): """Merge strategy: Keep higher tier for each gamemode""" result = {} # Add all players from both datasets for player in old_data + new_data: username_lower = player.get("username", "").lower() if username_lower not in result: result[username_lower] = { "username": player.get("username"), "region": player.get("region") } # Merge gamemode tiers - keep higher tier for key, value in player.items(): if key.startswith("tier_"): existing_tier = result[username_lower].get(key, "-") new_tier = value if get_tier_rank(new_tier) > get_tier_rank(existing_tier): result[username_lower][key] = new_tier else: result[username_lower][key] = existing_tier return list(result.values()) def overwrite_strategy(old_data, new_data): """Overwrite strategy: Old data as priority, fill gaps from new""" result = {} # First add all old data for player in old_data: username_lower = player.get("username", "").lower() result[username_lower] = {k: v for k, v in player.items() if k != "id"} # Then fill gaps from new data for player in new_data: username_lower = player.get("username", "").lower() if username_lower not in result: result[username_lower] = {k: v for k, v in player.items() if k != "id"} else: # Only add fields that don't exist in old data for key, value in player.items(): if key != "id" and not result[username_lower].get(key): result[username_lower][key] = value return list(result.values()) @app.route("/merge", methods=["POST"]) def merge_players(): """Merge two players, keeping higher tiers for each gamemode""" try: data = request.json print(f"[API] merge_players received: {data}") if not data.get("username1") or not data.get("username2"): return jsonify({ "success": False, "error": "username1 and username2 are required" }), 400 username1_lower = data["username1"].lower() # For comparison username2_lower = data["username2"].lower() # For comparison if username1_lower == username2_lower: return jsonify({ "success": False, "error": "Cannot merge a player with themselves" }), 400 url = f"{SUPABASE_URL}/rest/v1/players" headers = get_headers() # Fetch all players to find case-insensitive matches fetch_url = f"{url}?select=*" print(f"[API] Fetching all players for merge") fetch_response = requests.get(fetch_url, headers=headers) all_players = fetch_response.json() if fetch_response.status_code == 200 else [] # Find matching players (case-insensitive) player1_data = [p for p in all_players if p.get("username", "").lower() == username1_lower] player2_data = [p for p in all_players if p.get("username", "").lower() == username2_lower] if not player1_data or not player2_data: return jsonify({ "success": False, "error": f"One or both players not found" }), 404 player1 = player1_data[0] player2 = player2_data[0] print(f"[API] Player 1: {player1}") print(f"[API] Player 2: {player2}") # Get all gamemode columns (tier_*, excluding tier) gamemode_columns = [col for col in player1.keys() if col.startswith("tier_")] # Build merged update for player1 with higher tiers from both players merged_update = {} for gamemode_col in gamemode_columns: tier1 = player1.get(gamemode_col, "-") tier2 = player2.get(gamemode_col, "-") rank1 = get_tier_rank(tier1) rank2 = get_tier_rank(tier2) # Keep the higher tier if rank2 > rank1: merged_update[gamemode_col] = tier2 else: merged_update[gamemode_col] = tier1 print(f"[API] Merged update: {merged_update}") # Update player1 with merged data patch_url = f"{url}?id=eq.{player1['id']}" response = requests.patch(patch_url, json=merged_update, headers=headers) print(f"[API] PATCH response status: {response.status_code}") if response.status_code not in [200, 204]: return jsonify({ "success": False, "error": f"Failed to update player 1: {response.status_code}" }), 500 # Delete player2 delete_url = f"{url}?id=eq.{player2['id']}" delete_response = requests.delete(delete_url, headers=headers) print(f"[API] DELETE response status: {delete_response.status_code}") if delete_response.status_code not in [200, 204]: return jsonify({ "success": False, "error": f"Failed to delete player 2: {delete_response.status_code}" }), 500 return jsonify({ "success": True, "message": f"Successfully merged {data['username2']} into {data['username1']}", "merged_tiers": merged_update }), 200 except Exception as e: print(f"[API] merge_players error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @app.route("/retire", methods=["POST"]) def retire_gamemode(): """Retire a player in a specific gamemode""" try: data = request.json print(f"[API] retire received: {data}") if not data.get("username") or not data.get("gamemode"): return jsonify({ "success": False, "error": "username and gamemode are required" }), 400 username_lower = data["username"].lower() gamemode = data["gamemode"].lower() retired_column = f"retired_{gamemode}" url = f"{SUPABASE_URL}/rest/v1/players" headers = get_headers() # Fetch all players and find case-insensitive match fetch_url = f"{url}?select=id,username" fetch_response = requests.get(fetch_url, headers=headers) all_players = fetch_response.json() if fetch_response.status_code == 200 else [] matching_players = [p for p in all_players if p.get("username", "").lower() == username_lower] if not matching_players: return jsonify({ "success": False, "error": "Player not found" }), 404 player_id = matching_players[0]["id"] stored_username = matching_players[0]["username"] # Set the retired_{gamemode} column to 1 update_payload = {retired_column: 1} patch_url = f"{url}?id=eq.{player_id}" response = requests.patch(patch_url, json=update_payload, headers=headers) print(f"[API] Retire patch response status: {response.status_code}") if response.status_code in [200, 204]: return jsonify({ "success": True, "message": f"Player {stored_username} retired from {gamemode} gamemode" }), 200 else: print(f"[API] Patch error response: {response.text}") return jsonify({ "success": False, "error": f"Supabase error: {response.status_code}", "details": response.text[:500] if response.text else "No response body" }), response.status_code except Exception as e: print(f"[API] retire error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @app.route("/unretire", methods=["POST"]) def unretire_gamemode(): """Unretire a player in a specific gamemode""" try: data = request.json print(f"[API] unretire received: {data}") if not data.get("username") or not data.get("gamemode"): return jsonify({ "success": False, "error": "username and gamemode are required" }), 400 username_lower = data["username"].lower() gamemode = data["gamemode"].lower() retired_column = f"retired_{gamemode}" url = f"{SUPABASE_URL}/rest/v1/players" headers = get_headers() # Fetch all players and find case-insensitive match fetch_url = f"{url}?select=id,username" fetch_response = requests.get(fetch_url, headers=headers) all_players = fetch_response.json() if fetch_response.status_code == 200 else [] matching_players = [p for p in all_players if p.get("username", "").lower() == username_lower] if not matching_players: return jsonify({ "success": False, "error": "Player not found" }), 404 player_id = matching_players[0]["id"] stored_username = matching_players[0]["username"] # Set the retired_{gamemode} column to 0 update_payload = {retired_column: 0} patch_url = f"{url}?id=eq.{player_id}" response = requests.patch(patch_url, json=update_payload, headers=headers) print(f"[API] Unretire patch response status: {response.status_code}") if response.status_code in [200, 204]: return jsonify({ "success": True, "message": f"Player {stored_username} unretired from {gamemode} gamemode" }), 200 else: print(f"[API] Patch error response: {response.text}") return jsonify({ "success": False, "error": f"Supabase error: {response.status_code}", "details": response.text[:500] if response.text else "No response body" }), response.status_code except Exception as e: print(f"[API] unretire error: {str(e)}") return jsonify({"success": False, "error": str(e)}), 500 @app.route("/clear_gamemode", methods=["POST"]) def clear_gamemode(): """Clear player data for specific gamemode(s)""" try: if not SUPABASE_SERVICE_ROLE_KEY or "PASTE_YOUR_SERVICE_ROLE_KEY" in SUPABASE_SERVICE_ROLE_KEY: return jsonify({ "success": False, "error": "SUPABASE_SERVICE_ROLE_KEY not configured" }), 500 data = request.json gamemodes = data.get("gamemodes", []) backup_prefix = data.get("backup_prefix", "") if not gamemodes: return jsonify({ "success": False, "error": "No gamemodes specified" }), 400 print(f"[API] clear_gamemode: Clearing gamemodes: {gamemodes}") # Create backup with timestamp import os import json from datetime import datetime backup_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), "misc", "backups", "auto_clear")) os.makedirs(backup_dir, exist_ok=True) backup_file = os.path.join(backup_dir, f"{backup_prefix}_supabase.json") # Backup all current players url = f"{SUPABASE_URL}/rest/v1/players" headers = get_headers() fetch_url = f"{url}?select=*" fetch_response = requests.get(fetch_url, headers=headers) all_players = fetch_response.json() if fetch_response.status_code == 200 else [] # Save backup with open(backup_file, "w", encoding="utf-8") as f: json.dump(all_players, f, indent=4) print(f"[API] [OK] Backed up {len(all_players)} players to {backup_file}") # Delete players with specific gamemode tiers total_deleted = 0 for gamemode in gamemodes: tier_column = f"tier_{gamemode}" # Get players with this gamemode's tier for player in all_players: if player.get(tier_column): # Has this gamemode's tier player_id = player["id"] # Clear the tier column patch_url = f"{url}?id=eq.{player_id}" update_payload = {tier_column: None} # Set to NULL response = requests.patch(patch_url, json=update_payload, headers=headers) if response.status_code in [200, 204]: total_deleted += 1 print(f"[API] [OK] Cleared {tier_column} for player {player_id}") else: print(f"[API] [!] Failed to clear {tier_column} for player {player_id}") print(f"[API] [OK] Successfully cleared {total_deleted} player records for gamemodes: {gamemodes}") return jsonify({ "success": True, "backup_file": os.path.basename(backup_file), "records_cleared": total_deleted, "gamemodes_cleared": gamemodes }), 200 except Exception as e: print(f"[API] clear_gamemode error: {str(e)}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 @app.route("/restore_gamemode", methods=["POST"]) def restore_gamemode(): """Restore player data from gamemode backup""" try: if not SUPABASE_SERVICE_ROLE_KEY or "PASTE_YOUR_SERVICE_ROLE_KEY" in SUPABASE_SERVICE_ROLE_KEY: return jsonify({ "success": False, "error": "SUPABASE_SERVICE_ROLE_KEY not configured" }), 500 data = request.json backup_prefix = data.get("backup_prefix", "") if not backup_prefix: return jsonify({ "success": False, "error": "No backup_prefix specified" }), 400 print(f"[API] restore_gamemode: Restoring from backup {backup_prefix}") import os import json backup_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), "misc", "backups", "auto_clear")) backup_file = os.path.join(backup_dir, f"{backup_prefix}_supabase.json") if not os.path.exists(backup_file): print(f"[API] [ERROR] Backup file not found: {backup_file}") return jsonify({ "success": False, "error": f"Backup file not found" }), 404 # Load backup with open(backup_file, "r", encoding="utf-8") as f: backup_players = json.load(f) print(f"[API] Loaded {len(backup_players)} players from backup") url = f"{SUPABASE_URL}/rest/v1/players" headers = get_headers() total_restored = 0 # Restore each player for backup_player in backup_players: player_id = backup_player["id"] # Build update payload with all tier columns update_payload = {} for key, value in backup_player.items(): if key.startswith("tier_"): update_payload[key] = value if update_payload: patch_url = f"{url}?id=eq.{player_id}" response = requests.patch(patch_url, json=update_payload, headers=headers) if response.status_code in [200, 204]: total_restored += 1 print(f"[API] [OK] Restored player {player_id}") else: print(f"[API] [!] Failed to restore player {player_id}: {response.text[:200]}") print(f"[API] [OK] Successfully restored {total_restored} players") return jsonify({ "success": True, "records_restored": total_restored, "backup_prefix": backup_prefix }), 200 except Exception as e: print(f"[API] restore_gamemode error: {str(e)}") import traceback traceback.print_exc() return jsonify({"success": False, "error": str(e)}), 500 if __name__ == "__main__": init_logging() print(f"[KEY] Service Role Key loaded: {len(SUPABASE_SERVICE_ROLE_KEY) if SUPABASE_SERVICE_ROLE_KEY else 0} characters") print(f"[URL] Supabase URL: {SUPABASE_URL}") print(f"[API] Starting on localhost:5000") if log_file: log_file.write(f"[API] Starting server\n") log_file.write(f"[KEY] Service Role Key loaded: {len(SUPABASE_SERVICE_ROLE_KEY) if SUPABASE_SERVICE_ROLE_KEY else 0} characters\n") log_file.write(f"[URL] Supabase URL: {SUPABASE_URL}\n") log_file.write(f"[API] Listening on localhost:5000\n") app.run(host="localhost", port=5000, debug=False)