"""Server management routes: CRUD, bedtime, repair.""" import os import json from fastapi import APIRouter from fastapi.responses import JSONResponse import globals from server_manager import server_manager from routes.models import ServerConfigRequest from utils.logger import get_logger logger = get_logger('api') router = APIRouter() @router.get("/servers") def get_servers(): """Get all configured servers""" logger.debug("/servers endpoint called") logger.debug(f"server_manager.servers keys: {list(server_manager.servers.keys())}") logger.debug(f"server_manager.servers count: {len(server_manager.servers)}") config_file = server_manager.config_file logger.debug(f"Config file path: {config_file}") if os.path.exists(config_file): try: with open(config_file, "r", encoding="utf-8") as f: config_data = json.load(f) logger.debug(f"Config file contains: {list(config_data.keys())}") except Exception as e: logger.error(f"Failed to read config file: {e}") else: logger.warning("Config file does not exist") servers = [] for server in server_manager.get_all_servers(): server_data = server.to_dict() server_data['enabled_features'] = list(server_data['enabled_features']) server_data['guild_id'] = str(server_data['guild_id']) servers.append(server_data) logger.debug(f"Adding server to response: {server_data['guild_id']} - {server_data['guild_name']}") logger.debug(f"Server data type check - guild_id: {type(server_data['guild_id'])}, value: {server_data['guild_id']}") logger.debug(f"Returning {len(servers)} servers") return {"servers": servers} @router.post("/servers") def add_server(data: ServerConfigRequest): """Add a new server configuration""" enabled_features = set(data.enabled_features) if data.enabled_features else None success = server_manager.add_server( guild_id=data.guild_id, guild_name=data.guild_name, autonomous_channel_id=data.autonomous_channel_id, autonomous_channel_name=data.autonomous_channel_name, bedtime_channel_ids=data.bedtime_channel_ids, enabled_features=enabled_features ) if success: server_manager.stop_all_schedulers() server_manager.start_all_schedulers(globals.client) return {"status": "ok", "message": f"Server {data.guild_name} added successfully"} else: return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to add server"}) @router.delete("/servers/{guild_id}") def remove_server(guild_id: int): """Remove a server configuration""" success = server_manager.remove_server(guild_id) if success: return {"status": "ok", "message": "Server removed successfully"} else: return JSONResponse(status_code=404, content={"status": "error", "message": "Failed to remove server"}) @router.put("/servers/{guild_id}") def update_server(guild_id: int, data: dict): """Update server configuration""" success = server_manager.update_server_config(guild_id, **data) if success: server_manager.stop_all_schedulers() server_manager.start_all_schedulers(globals.client) return {"status": "ok", "message": "Server configuration updated"} else: return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to update server configuration"}) @router.post("/servers/{guild_id}/bedtime-range") def update_server_bedtime_range(guild_id: int, data: dict): """Update server bedtime range configuration""" logger.debug(f"Updating bedtime range for server {guild_id}: {data}") required_fields = ['bedtime_hour', 'bedtime_minute', 'bedtime_hour_end', 'bedtime_minute_end'] for field in required_fields: if field not in data: return JSONResponse(status_code=400, content={"status": "error", "message": f"Missing required field: {field}"}) try: bedtime_hour = int(data['bedtime_hour']) bedtime_minute = int(data['bedtime_minute']) bedtime_hour_end = int(data['bedtime_hour_end']) bedtime_minute_end = int(data['bedtime_minute_end']) if not (0 <= bedtime_hour <= 23) or not (0 <= bedtime_hour_end <= 23): return JSONResponse(status_code=400, content={"status": "error", "message": "Hours must be between 0 and 23"}) if not (0 <= bedtime_minute <= 59) or not (0 <= bedtime_minute_end <= 59): return JSONResponse(status_code=400, content={"status": "error", "message": "Minutes must be between 0 and 59"}) except (ValueError, TypeError): return JSONResponse(status_code=400, content={"status": "error", "message": "Invalid time values provided"}) success = server_manager.update_server_config(guild_id, **data) if success: job_success = server_manager.update_server_bedtime_job(guild_id, globals.client) if job_success: logger.info(f"Bedtime range updated for server {guild_id}") return { "status": "ok", "message": f"Bedtime range updated: {bedtime_hour:02d}:{bedtime_minute:02d} - {bedtime_hour_end:02d}:{bedtime_minute_end:02d}" } else: return JSONResponse(status_code=500, content={"status": "error", "message": "Updated config but failed to update scheduler"}) else: return JSONResponse(status_code=500, content={"status": "error", "message": "Failed to update bedtime range"}) @router.post("/servers/repair") def repair_server_config(): """Repair corrupted server configuration""" try: server_manager.repair_config() return {"status": "ok", "message": "Server configuration repaired and saved"} except Exception as e: return JSONResponse(status_code=500, content={"status": "error", "message": f"Failed to repair configuration: {e}"}) @router.post("/servers/recover") def recover_servers_from_discord(): """Auto-discover servers from Discord guilds and create config entries. Use this when servers_config.json is lost/corrupted and you need to quickly restore basic server configurations. Each discovered guild gets a placeholder config using the first available text channel as the autonomous channel. You can then adjust channels via the dashboard. """ if not globals.client or not globals.client.is_ready(): return JSONResponse(status_code=503, content={ "status": "error", "message": "Discord client not ready — bot must be connected" }) if not globals.client.guilds: return JSONResponse(status_code=404, content={ "status": "error", "message": "Bot is not in any Discord guilds" }) recovered = [] skipped = [] failed = [] for guild in globals.client.guilds: guild_id = guild.id guild_name = guild.name # Skip if already configured if server_manager.get_server_config(guild_id): skipped.append({"guild_id": str(guild_id), "guild_name": guild_name, "reason": "Already configured"}) continue # Find the first text channel (prefer one named "general" or "chat") text_channels = [ch for ch in guild.text_channels if ch.permissions_for(guild.me).send_messages] if not text_channels: # Try any text channel even without send permissions text_channels = guild.text_channels if not text_channels: failed.append({"guild_id": str(guild_id), "guild_name": guild_name, "reason": "No text channels found"}) continue # Prefer "general" or "chat" channel, otherwise use the first one preferred = None for ch in text_channels: if ch.name.lower() in ("general", "chat", "main", "lounge", "general-chat"): preferred = ch break channel = preferred or text_channels[0] try: success = server_manager.add_server( guild_id=guild_id, guild_name=guild_name, autonomous_channel_id=channel.id, autonomous_channel_name=f"#{channel.name}", bedtime_channel_ids=[channel.id], enabled_features={"autonomous", "bedtime", "monday_video"} ) if success: recovered.append({ "guild_id": str(guild_id), "guild_name": guild_name, "autonomous_channel": f"#{channel.name} ({channel.id})" }) logger.info(f"Recovered server config: {guild_name} (ID: {guild_id}) → #{channel.name}") else: failed.append({"guild_id": str(guild_id), "guild_name": guild_name, "reason": "add_server returned False"}) except Exception as e: failed.append({"guild_id": str(guild_id), "guild_name": guild_name, "reason": str(e)}) logger.error(f"Failed to recover server {guild_name}: {e}") # Restart schedulers if we recovered any servers if recovered: try: server_manager.stop_all_schedulers() server_manager.start_all_schedulers(globals.client) except Exception as e: logger.error(f"Failed to restart schedulers after recovery: {e}") return { "status": "ok", "recovered": recovered, "skipped": skipped, "failed": failed, "total_guilds": len(globals.client.guilds), "note": "Recovered servers use the first text channel as autonomous channel. " "Use the Servers tab to adjust channel settings." }