", methods=["POST"])
async def api_confiscate(instance_id):
if not session.get("admin_logged_in"): return jsonify({"error": "Unauthorized"}), 403
async with db.pool.acquire() as conn:
async with conn.transaction():
exists = await conn.fetchval("SELECT 1 FROM user_characters WHERE instance_id = $1", instance_id)
if not exists:
return jsonify({"status": "error", "message": "Card not found"})
# Wipe it from any visual profile slots before deleting
await conn.execute("UPDATE users SET showcase_id = NULL WHERE showcase_id = $1", instance_id)
for i in range(1, 9):
await conn.execute(f"UPDATE users SET profile_slot_{i} = NULL WHERE profile_slot_{i} = $1", instance_id)
# The ON DELETE CASCADE in your schema automatically handles user_teams, expeditions, and the market
await conn.execute("DELETE FROM user_characters WHERE instance_id = $1", instance_id)
return jsonify({"status": "success"})
@app.route("/recover_db")
async def recover_db():
if not session.get("admin_logged_in"): return redirect("/login")
CLEAN_KEY = str(SUPABASE_KEY).strip(' \n\r"\'')
if CLEAN_KEY.startswith("sb_"):
CLEAN_KEY = CLEAN_KEY[3:]
recovered_count = 0
offset = 0
limit = 1000
async with httpx.AsyncClient() as client:
async with db.pool.acquire() as conn:
while True:
url = f"{SUPABASE_URL}/storage/v1/object/list/{BUCKET_NAME}"
headers = {
"Authorization": f"Bearer {CLEAN_KEY}",
"apikey": CLEAN_KEY,
"Content-Type": "application/json"
}
payload = {"prefix": "", "limit": limit, "offset": offset, "sortBy": {"column": "name", "order": "asc"}}
resp = await client.post(url, headers=headers, json=payload)
if resp.status_code != 200:
safe_key_preview = CLEAN_KEY[:15] + "..." if len(CLEAN_KEY) > 15 else "TOO_SHORT"
error_html = f"""
🛑 STILL BLOCKED!
Status Code: {resp.status_code}
{resp.text}
Key Preview (first 15 chars): {safe_key_preview}
"""
return error_html
files = resp.json()
if not isinstance(files, list) or len(files) == 0:
break
for file in files:
filename = file.get("name")
if not filename or filename.startswith(".emptyFolder"):
continue
public_url = f"{SUPABASE_URL}/storage/v1/object/public/{BUCKET_NAME}/{filename}"
exists = await conn.fetchval("SELECT 1 FROM characters WHERE image_url = $1", public_url)
if exists:
continue
clean_filename = filename.replace(".webp", "").replace(".png", "").replace(".jpg", "")
parts = clean_filename.split("_")
if parts[0] == "web": name_parts = parts[2:]
elif parts[0].isdigit() and len(parts) > 2: name_parts = parts[2:]
else: name_parts = parts
char_name = " ".join(name_parts).title()
if not char_name: char_name = "Unknown"
await conn.execute(
"INSERT INTO characters (name, rarity, element, atk, def, image_url, series, source) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
char_name, "C", "None", 50, 50, public_url, "Recovered Series", "RecoveryScript"
)
recovered_count += 1
if len(files) < limit:
break
offset += limit
from services.gacha_service import GachaService
await GachaService.rebuild_pools()
html = f"""
✅ Resurrection Complete!
Successfully scanned the storage bucket and linked {recovered_count} images back into the database.
Return to Characters
"""
return html
@app.route("/api/bulk_delete", methods=["POST"])
async def bulk_delete_characters():
if not session.get("admin_logged_in"): return jsonify({"error": "Unauthorized"}), 403
data = await request.json
char_ids = data.get("char_ids", [])
if not char_ids: return jsonify({"status": "error", "message": "No characters selected."})
try: char_ids = [int(cid) for cid in char_ids]
except ValueError: return jsonify({"status": "error", "message": "Invalid ID format."})
async with db.pool.acquire() as conn:
protected_rows = await conn.fetch("SELECT char_id FROM user_characters WHERE char_id = ANY($1) GROUP BY char_id HAVING COUNT(*) > 5", char_ids)
protected_ids = {r['char_id'] for r in protected_rows}
to_delete = [cid for cid in char_ids if cid not in protected_ids]
if not to_delete:
return jsonify({"status": "protected", "message": f"All {len(char_ids)} selected characters are protected (owned by >5 players)."})
char_data = await conn.fetch("SELECT image_url FROM characters WHERE char_id = ANY($1)", to_delete)
if SUPABASE_URL and SUPABASE_KEY:
headers = {"Authorization": f"Bearer {SUPABASE_KEY}", "apikey": SUPABASE_KEY}
async def delete_storage_files():
async with httpx.AsyncClient() as client:
tasks = []
for row in char_data:
if row['image_url']:
filename = row['image_url'].split('/')[-1]
delete_url = f"{SUPABASE_URL}/storage/v1/object/{BUCKET_NAME}/{filename}"
tasks.append(client.delete(delete_url, headers=headers))
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
asyncio.create_task(delete_storage_files())
async with conn.transaction():
await conn.execute("UPDATE users SET showcase_id = NULL WHERE showcase_id IN (SELECT instance_id FROM user_characters WHERE char_id = ANY($1))", to_delete)
await conn.execute("UPDATE user_teams SET slot_1 = NULL WHERE slot_1 IN (SELECT instance_id FROM user_characters WHERE char_id = ANY($1))", to_delete)
await conn.execute("UPDATE user_teams SET slot_2 = NULL WHERE slot_2 IN (SELECT instance_id FROM user_characters WHERE char_id = ANY($1))", to_delete)
await conn.execute("UPDATE user_teams SET slot_3 = NULL WHERE slot_3 IN (SELECT instance_id FROM user_characters WHERE char_id = ANY($1))", to_delete)
await conn.execute("DELETE FROM expeditions WHERE instance_id IN (SELECT instance_id FROM user_characters WHERE char_id = ANY($1))", to_delete)
await conn.execute("DELETE FROM user_characters WHERE char_id = ANY($1)", to_delete)
await conn.execute("DELETE FROM characters WHERE char_id = ANY($1)", to_delete)
from services.gacha_service import GachaService
await GachaService.rebuild_pools()
return jsonify({
"status": "success",
"deleted": len(to_delete),
"protected": len(protected_ids),
"message": f"Successfully deleted {len(to_delete)} characters."
})