Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/apkvox_bet.json
T

1 line
31 KiB
JSON

{"repo": "apkvox/bet", "files": {"main.py": {"c10dd431ff3f99946f59ddb18d8761234ec462fd": {"t": 1785674059.375199, "n": 30029, "h": "5a674218377c1651", "c": "\"\"\"\n===========================================\nNBA PREDICTOR AI - MAIN API (LIGHT VERSION)\n===========================================\nEsta versi├│n est├í optimizada para consumir <50MB RAM en Render.\nSolo expone las APIs leyendo desde SQLite (history.db).\nEl trabajo pesado se movi├│ a GitHub Actions (generate_daily_job.py).\n\"\"\"\n\nimport os\nimport sys\nimport asyncio\nimport threading\nfrom datetime import datetime, timezone, timedelta\nfrom typing import Optional\n\n# Zona horaria de Colombia (UTC-5)\nTZ_COLOMBIA = timezone(timedelta(hours=-5))\n\nfrom fastapi import FastAPI, BackgroundTasks, HTTPException, Request, Depends\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.staticfiles import StaticFiles\nfrom fastapi.responses import FileResponse, Response\nfrom pydantic import BaseModel\nimport uvicorn\n\n# Modules that only contain DB read logic\nimport history_db\n\nimport admin_auth\nimport site_settings\n\n# Servicio que actualiza marcadores de partidos pasados (PENDING -> WIN/LOSS)\n_last_history_update_run: Optional[datetime] = None\n_HISTORY_UPDATE_COOLDOWN_MINUTES = 10\n\ndef _run_history_update():\n global _last_history_update_run\n _last_history_update_run = datetime.now(timezone.utc)\n try:\n from src.Services.history_service import update_pending_predictions\n update_pending_predictions()\n except Exception as e:\n print(f\"[main] Error actualizando historial (no cr├¡tico): {e}\")\n\n# ===========================================\n# CONFIGURACI├ôN\n# ===========================================\nDEBUG_MODE = os.getenv(\"DEBUG_MODE\", \"false\").lower() == \"true\"\nRENDER_EXTERNAL_URL = os.getenv(\"RENDER_EXTERNAL_URL\", \"https://bet-7b8l.onrender.com\")\n\nclass MatchPrediction(BaseModel):\n home_team: str\n away_team: str\n winner: str\n win_probability: float\n under_over: str\n ou_line: Optional[float] = None\n ou_probability: float\n ai_analysis: Optional[str] = None\n is_mock: bool = False\n \n odds_home: Optional[float] = None\n odds_away: Optional[float] = None\n implied_prob: Optional[float] = None\n ev_score: Optional[float] = None\n kelly_stake: Optional[float] = None\n discrepancy: Optional[float] = None\n warning_level: Optional[str] = None\n value_type: Optional[str] = None\n sentiment_score: Optional[float] = None\n key_injuries: Optional[list] = None\n risk_analysis: Optional[str] = None\n game_status: Optional[str] = None\n home_score: Optional[int] = None\n away_score: Optional[int] = None\n\nclass PredictionResponse(BaseModel):\n date: str\n total_games: int\n predictions: list[MatchPrediction]\n model_accuracy: str\n status: str\n\n# ===========================================\n# INICIALIZACI├ôN DE FASTAPI\n# ===========================================\napp = FastAPI(\n title=\"La Fija API\",\n description=\"API de predicciones deportivas pre-calculadas\",\n version=\"2.0.0\"\n)\n\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\n \"http://localhost:8000\",\n \"http://127.0.0.1:8000\",\n \"http://localhost:8080\",\n \"http://127.0.0.1:8080\",\n \"http://localhost:3000\",\n \"http://localhost:8081\",\n \"exp://192.168.18.9:8081\",\n \"https://bet-7b8l.onrender.com\",\n \"https://lafija.web.app\" # Posible futuro dominio\n ],\n allow_credentials=True,\n allow_methods=[\"GET\", \"POST\", \"OPTIONS\"],\n allow_headers=[\"*\"],\n)\n\n@app.middleware(\"http\")\nasync def add_security_headers(request: Request, call_next):\n response = await call_next(request)\n response.headers[\"X-Frame-Options\"] = \"SAMEORIGIN\"\n response.headers[\"X-Content-Type-Options\"] = \"nosniff\"\n response.headers[\"Strict-Transport-Security\"] = \"max-age=31536000; includeSubDomains\"\n return response\n\n\n# Montar archivos est├íticos\nos.makedirs(\"static\", exist_ok=True)\napp.mount(\"/static\", StaticFiles(directory=\"static\"), name=\"static\")\n\n\n@app.on_event(\"startup\")\nasync def startup_event():\n print(\"===========================================\")\n print(\"La Fija API arrancando...\")\n print(\"===========================================\")\n history_db.init_history_db()\n \n # 1. Actualizar marcadores en background\n threading.Thread(target=_run_history_update, daemon=True).start()\n \n # 2. Disparar noticias y combinada del día si faltan\n async def _auto_news_job():\n try:\n from generate_daily_job import run_news_and_recommendations\n await run_news_and_recommendations()\n except Exception as e:\n print(f\"[startup] Error en job automático: {e}\")\n \n asyncio.create_task(_auto_news_job())\n\n\n# ===========================================\n# RUTAS DE LA API (FRONTEND Y LECTURA)\n# ===========================================\n\n@app.get(\"/\")\nasync def serve_frontend():\n \"\"\"Sirve la Single Page Application.\"\"\"\n return FileResponse('static/index.html')\n\n\n@app.get(\"/admin\")\nasync def serve_admin():\n \"\"\"Sirve la pantalla de login de administración.\"\"\"\n return FileResponse('static/admin.html')\n\n\n@app.get(\"/promo-editor\")\nasync def serve_promo_editor():\n \"\"\"Editor visual de promos (requiere haber hecho login en /admin).\"\"\"\n return FileResponse('static/promo_editor.html')\n\n\n@app.get(\"/api/settings\")\nasync def get_public_settings():\n \"\"\"Configuración pública (tema, branding, anuncios) para el frontend.\"\"\"\n return site_settings.get_public_settings()\n\n\n# -------------------------------------------\n# Admin API (login + cuenta inicial)\n# -------------------------------------------\n\n\ndef _admin_token(request: Request) -> Optional[str]:\n auth = request.headers.get(\"Authorization\")\n if auth and auth.startswith(\"Bearer \"):\n return auth[7:].strip()\n return None\n\n\ndef require_admin(request: Request):\n \"\"\"Valida JWT de administrador desde Authorization: Bearer <token>.\"\"\"\n token = _admin_token(request)\n if not token or not admin_auth.verify_token(token):\n raise HTTPException(status_code=401, detail=\"Sesión inválida o expirada\")\n\n\n@app.get(\"/api/admin/status\")\nasync def admin_status():\n \"\"\"\n Devuelve el estado básico del módulo de administración:\n - has_admin: si ya existe una cuenta admin\n - token_ttl_hours: duración de los JWT\n \"\"\"\n return {\n \"has_admin\": admin_auth.has_admin(),\n \"token_ttl_hours\": admin_auth.TOKEN_EXPIRY_HOURS,\n }\n\n\n@app.post(\"/api/admin/create-initial\")\nasync def admin_create_initial(request: Request):\n \"\"\"\n Crea la cuenta inicial de administrador.\n Solo se permite si todavía no existe un admin.\n Body: { \"password\": \"...\" }\n \"\"\"\n try:\n body = await request.json()\n except Exception:\n body = {}\n\n password = (body.get(\"password\") or \"\").strip()\n if not password:\n raise HTTPException(status_code=400, detail=\"Falta contraseña\")\n\n if admin_auth.has_admin():\n raise HTTPException(status_code=400, detail=\"Ya existe una cuenta de administrador\")\n\n try:\n admin_auth.create_initial_admin(password)\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n except Exception as e:\n from config_store import ConfigStoreError\n print(f\"[main] create-initial error: {type(e).__name__}: {e}\")\n detail = str(e) if isinstance(e, ConfigStoreError) else \"Error al guardar. Revisa DATABASE_URL y la tabla app_config en Supabase.\"\n raise HTTPException(status_code=500, detail=detail)\n\n return {\"status\": \"ok\", \"message\": \"Cuenta de administrador creada correctamente\"}\n\n\n@app.post(\"/api/admin/login\")\nasync def admin_login(request: Request):\n \"\"\"\n Login admin con contraseña; devuelve un JWT.\n Body: { \"password\": \"...\" }\n \"\"\"\n try:\n body = await request.json()\n except Exception:\n body = {}\n\n password = (body.get(\"password\") or \"\").strip()\n if not password:\n raise HTTPException(status_code=400, detail=\"Falta contraseña\")\n\n ip = request.client.host if request.client else \"unknown\"\n if not admin_auth.check_rate_limit(ip):\n raise HTTPException(status_code=429, detail=\"Demasiados intentos. Espera 15 minutos.\")\n\n try:\n token = admin_auth.login_with_password(password)\n except ValueError as e:\n admin_auth.record_attempt(ip)\n raise HTTPException(status_code=401, detail=str(e))\n\n return {\"token\": token}\n\n\n@app.post(\"/api/admin/change-password\")\nasync def admin_change_password(request: Request, _: None = Depends(require_admin)):\n \"\"\"Cambia la contraseña del admin validando la actual.\"\"\"\n try:\n body = await request.json()\n except Exception:\n body = {}\n\n current = (body.get(\"current_password\") or \"\").strip()\n new_pwd = (body.get(\"new_password\") or \"\").strip()\n\n if not current or not new_pwd:\n raise HTTPException(status_code=400, detail=\"Faltan campos obligatorios\")\n\n try:\n admin_auth.change_password(current, new_pwd)\n except ValueError as e:\n raise HTTPException(status_code=400, detail=str(e))\n\n return {\"status\": \"ok\", \"message\": \"Contraseña actualizada correctamente\"}\n\n\n@app.get(\"/api/admin/settings\")\nasync def admin_get_settings(_: None = Depends(require_admin)):\n \"\"\"\n Obtiene la configuración completa del sitio para el panel de administración.\n No incluye ninguna credencial sensible.\n \"\"\"\n return site_settings.load_settings()\n\n\n# Límites para evitar 413 en Render (payload demasiado grande)\n_MAX_ANNOUNCEMENT_LEN = 500\n_MAX_BRANDING_SUBTITLE_LEN = 200\n\n\n@app.post(\"/api/admin/settings\")\nasync def admin_save_settings(request: Request, _: None = Depends(require_admin)):\n \"\"\"\n Guarda la configuración del sitio.\n Solo se actualizan las claves de alto nivel conocidas.\n Campos largos se truncan para evitar 413.\n \"\"\"\n try:\n body = await request.json()\n except Exception:\n body = None\n\n if not isinstance(body, dict) or not body:\n raise HTTPException(status_code=400, detail=\"Body vacío o inválido\")\n\n # Truncar campos que pueden crecer y causar 413\n if \"announcement\" in body and isinstance(body[\"announcement\"], dict):\n t = body[\"announcement\"].get(\"text\", \"\")\n if isinstance(t, str) and len(t) > _MAX_ANNOUNCEMENT_LEN:\n body[\"announcement\"] = {**body[\"announcement\"], \"text\": t[:_MAX_ANNOUNCEMENT_LEN]}\n if \"branding\" in body and isinstance(body[\"branding\"], dict):\n s = body[\"branding\"].get(\"subtitle\", \"\")\n if isinstance(s, str) and len(s) > _MAX_BRANDING_SUBTITLE_LEN:\n body[\"branding\"] = {**body[\"branding\"], \"subtitle\": s[:_MAX_BRANDING_SUBTITLE_LEN]}\n\n cfg = site_settings.load_settings()\n for key in (\"theme\", \"branding\", \"features\", \"announcement\", \"ads\", \"betting\"):\n if key in body:\n cfg[key] = body[key]\n\n site_settings.save_settings(cfg)\n return {\"status\": \"ok\"}\n\n\n@app.get(\"/api/health\")\nasync def health_check():\n \"\"\"Endpoint simple para verificar que la DB responde.\"\"\"\n return {\"status\": \"online\", \"db_connected\": True}\n\n\n# -------------------------------------------\n# Promo editor preview + config (admin y cards)\n# -------------------------------------------\ndef _parse_promo_query(request: Request) -> tuple:\n \"\"\"Extrae home_team, away_team, winner, probability y config_override de query params.\"\"\"\n q = request.query_params\n home_team = q.get(\"home_team\", \"Charlotte Hornets\")\n away_team = q.get(\"away_team\", \"Dallas Mavericks\")\n winner = q.get(\"winner\", home_team)\n prob_s = q.get(\"probability\", \"58.1\")\n try:\n probability = float(prob_s)\n except ValueError:\n probability = 58.1\n config_override = {}\n int_keys = (\n \"logo_cy\", \"logo_left_cx\", \"logo_right_offset\", \"logo_max\",\n \"names_y\", \"names_font_size\", \"names_max_w\",\n \"box_y0\", \"box_y1\", \"box_pad_x\", \"box_radius\", \"box_border_w\",\n \"label_offset_y\", \"label_font_size\",\n \"winner_offset_y\", \"winner_font_size\",\n \"prob_offset_y\", \"prob_font_size\",\n \"footer_y\", \"footer_font_size\",\n )\n color_keys = (\"names_color\", \"box_border_color\", \"label_color\", \"winner_color\", \"prob_color\", \"footer_color\")\n for k in int_keys:\n v = q.get(k)\n if v is not None:\n try:\n config_override[k] = int(v)\n except ValueError:\n pass\n for k in color_keys:\n v = q.get(k)\n if v:\n config_override[k] = v\n for toggle in (\"show_logos\", \"show_names\", \"show_box_border\", \"show_label\", \"show_winner\", \"show_prob\", \"show_footer\"):\n v = q.get(toggle)\n if v is not None:\n config_override[toggle] = str(v).lower() in (\"true\", \"1\", \"yes\", \"on\")\n return home_team, away_team, winner, probability, config_override\n\n\n@app.get(\"/api/promo-editor-preview\")\nasync def promo_editor_preview_get(request: Request):\n \"\"\"Preview por GET (query params). Para URLs largas usar POST.\"\"\"\n try:\n from promo_generator import generate_promo_image\n home_team, away_team, winner, probability, config_override = _parse_promo_query(request)\n data = generate_promo_image(home_team, away_team, winner, probability, status=None, config_override=config_override or None)\n return Response(content=data, media_type=\"image/png\")\n except Exception as e:\n print(f\"[main] promo-editor-preview error: {e}\")\n raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.post(\"/api/promo-editor-preview\")\nasync def promo_editor_preview_post(request: Request):\n \"\"\"Preview por POST (body JSON). Evita 413 por URL demasiado larga en Render.\"\"\"\n try:\n body = await request.json()\n if not isinstance(body, dict):\n raise HTTPException(400, \"Body debe ser JSON object\")\n home_team = str(body.get(\"home_team\", \"Charlotte Hornets\"))[:80]\n away_team = str(body.get(\"away_team\", \"Dallas Mavericks\"))[:80]\n winner = str(body.get(\"winner\", home_team))[:80]\n probability = float(body.get(\"probability\", 58.1))\n config_override = {k: v for k, v in body.items() if k not in (\"home_team\", \"away_team\", \"winner\", \"probability\")}\n from promo_generator import generate_promo_image\n data = generate_promo_image(home_team, away_team, winner, probability, status=None, config_override=config_override or None)\n return Response(content=data, media_type=\"image/png\")\n except HTTPException:\n raise\n except Exception as e:\n print(f\"[main] promo-editor-preview POST error: {e}\")\n raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.get(\"/api/promo-image\")\nasync def promo_image(request: Request):\n \"\"\"Genera imagen de promo para una predicción (para descargar desde las cards).\"\"\"\n try:\n from promo_generator import generate_promo_image\n home_team = request.query_params.get(\"home_team\", \"\")\n away_team = request.query_params.get(\"away_team\", \"\")\n winner = request.query_params.get(\"winner\", \"\")\n prob_s = request.query_params.get(\"probability\", \"0\")\n status = request.query_params.get(\"status\") # PENDING, WIN, LOSS -> Ganada/Perdida\n try:\n probability = float(prob_s)\n except ValueError:\n probability = 0.0\n if not home_team or not away_team or not winner:\n raise HTTPException(status_code=400, detail=\"Faltan home_team, away_team o winner\")\n status_label = None\n if status and str(status).upper() in (\"WIN\", \"GANADA\"):\n status_label = \"GANADA\"\n elif status and str(status).upper() in (\"LOSS\", \"PERDIDA\"):\n status_label = \"PERDIDA\"\n data = generate_promo_image(home_team, away_team, winner, probability, status=status_label)\n return Response(content=data, media_type=\"image/png\")\n except HTTPException:\n raise\n except Exception as e:\n print(f\"[main] promo-image error: {e}\")\n raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.get(\"/api/promo-config\")\nasync def get_promo_config(_: None = Depends(require_admin)):\n \"\"\"Devuelve la configuración del editor de promo (layout).\"\"\"\n try:\n from promo_generator import load_config\n return load_config()\n except Exception as e:\n print(f\"[main] get promo-config error: {e}\")\n return {}\n\n\n_PROMO_CONFIG_KEYS = frozenset((\n \"logo_cy\", \"logo_left_cx\", \"logo_right_offset\", \"logo_max\",\n \"names_y\", \"names_font_size\", \"names_max_w\", \"names_color\",\n \"box_y0\", \"box_y1\", \"box_pad_x\", \"box_radius\", \"box_border_w\", \"box_border_color\",\n \"label_offset_y\", \"label_font_size\", \"label_color\",\n \"winner_offset_y\", \"winner_font_size\", \"winner_color\",\n \"prob_offset_y\", \"prob_font_size\", \"prob_color\",\n \"footer_y\", \"footer_font_size\", \"footer_color\",\n \"show_logos\", \"show_names\", \"show_box_border\", \"show_label\", \"show_winner\", \"show_prob\", \"show_footer\",\n))\n\n\n@app.post(\"/api/promo-config\")\nasync def save_promo_config(request: Request, _: None = Depends(require_admin)):\n \"\"\"Guarda la configuración del editor de promo. Solo claves conocidas (evita 413).\"\"\"\n try:\n body = await request.json()\n if not isinstance(body, dict):\n raise HTTPException(400, \"Body debe ser JSON object\")\n cfg = {k: v for k, v in body.items() if k in _PROMO_CONFIG_KEYS}\n from promo_generator import save_config\n save_config(cfg)\n return {\"status\": \"ok\"}\n except HTTPException:\n raise\n except Exception as e:\n print(f\"[main] save promo-config error: {e}\")\n raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.get(\"/predict-today\")\nasync def predict_today():\n \"\"\"\n Obtiene las predicciones NBA generadas hoy desde la BD.\n Si no est├ín listas, las genera en tiempo real (Self-Healing).\n \"\"\"\n date_str = datetime.now(TZ_COLOMBIA).strftime('%Y-%m-%d')\n existing_preds = history_db.get_predictions_by_date_light(date_str)\n \n if not existing_preds:\n print(\"No hay datos NBA en SQLite. Generando On-Demand...\")\n try:\n from generate_daily_job import generate_nba_predictions\n # Generar predicciones en tiempo real de forma as├¡ncrona pero esperando resultado\n new_preds = await generate_nba_predictions()\n if new_preds:\n # Reload from DB specifically to get the light DB format to match frontend expectations\n existing_preds = history_db.get_predictions_by_date_light(date_str)\n except Exception as e:\n print(f\"Error generando NBA on-demand: {e}\")\n\n if existing_preds:\n print(f\"Sirviendo {len(existing_preds)} juegos NBA\")\n return {\n \"date\": date_str,\n \"total_games\": len(existing_preds),\n \"predictions\": existing_preds,\n \"model_accuracy\": \"70% (Offline)\",\n \"status\": \"success\"\n }\n else:\n return {\n \"date\": date_str,\n \"total_games\": 0,\n \"predictions\": [],\n \"model_accuracy\": \"N/A\",\n \"status\": \"no_games_today\"\n }\n\n\n@app.get(\"/predict-football\")\nasync def predict_football():\n \"\"\"\n Obtiene las predicciones de F├║tbol preparadas por la BD o las genera On-Demand.\n \"\"\"\n import traceback, logging\n logger = logging.getLogger(__name__)\n try:\n preds = history_db.get_upcoming_football_predictions()\n \n if not preds:\n logger.info(\"No hay datos F├║tbol en SQLite. Generando On-Demand...\")\n try:\n from generate_daily_job import generate_football_predictions\n new_preds = await generate_football_predictions()\n if new_preds:\n preds = history_db.get_upcoming_football_predictions()\n except Exception as e:\n logger.error(f\"Error generando F├║tbol on-demand: {e}\")\n\n if preds:\n try:\n from football_logos import get_team_info\n for p in preds:\n home_info = get_team_info(p['home_team'])\n away_info = get_team_info(p['away_team'])\n p['home_team'] = home_info.get('name', p['home_team'])\n p['away_team'] = away_info.get('name', p['away_team'])\n p['home_logo'] = home_info.get('logo')\n p['away_logo'] = away_info.get('logo')\n except Exception as logo_err:\n logger.warning(f\"football_logos enrichment failed (non-fatal): {logo_err}\")\n\n return {\"status\": \"success\", \"count\": len(preds), \"predictions\": preds}\n else:\n return {\"status\": \"pending_github_actions\", \"count\": 0, \"predictions\": []}\n except Exception as e:\n tb = traceback.format_exc()\n logger.error(f\"predict-football FAILED:\\n{tb}\")\n return {\"status\": \"error\", \"detail\": str(e), \"traceback\": tb, \"count\": 0, \"predictions\": []}\n\n\n@app.post(\"/history/refresh\")\nasync def refresh_history_scores():\n \"\"\"Fuerza la actualizaci├│n de marcadores y predicciones de hoy. (On-Demand Refresh)\"\"\"\n try:\n from src.Services.history_service import update_pending_predictions\n from history_db import delete_predictions_for_date\n from generate_daily_job import generate_nba_predictions, generate_football_predictions\n \n # 1. Update old results\n result = await asyncio.to_thread(update_pending_predictions)\n \n # 2. Force delete today's pending matches to clear old odds\n today_str = datetime.now(TZ_COLOMBIA).strftime('%Y-%m-%d')\n deleted_count = await asyncio.to_thread(delete_predictions_for_date, today_str)\n print(f\"[main] Eliminadas {deleted_count} predicciones antiguas para el d├¡a de hoy.\")\n \n # 3. Trigger fresh generation\n await generate_nba_predictions()\n await generate_football_predictions()\n \n result['status'] = 'success'\n result['message'] = f'Marcadores actualizados y cuotas refrescadas para hoy.'\n return result\n except Exception as e:\n msg = str(e)\n print(f\"[main] Error en refresh historial: {e}\")\n raise HTTPException(status_code=500, detail=msg)\n\n\n@app.post(\"/api/admin/force-correct\")\nasync def admin_force_correct(_: None = Depends(require_admin)):\n \"\"\"\n Fuerza una autocorrección del sistema:\n 1. Borra TODAS las predicciones de hoy (correctas o incorrectas).\n 2. Actualiza resultados pendientes de días pasados.\n 3. Regenera predicciones NBA y Fútbol desde cero.\n Solo accesible para administradores.\n \"\"\"\n try:\n from history_db import force_delete_all_predictions_for_date\n from generate_daily_job import generate_nba_predictions, generate_football_predictions, update_past_results\n\n today_str = datetime.now(TZ_COLOMBIA).strftime('%Y-%m-%d')\n\n # 1. Purge ALL today's predictions (not just PENDING)\n deleted = await asyncio.to_thread(force_delete_all_predictions_for_date, today_str)\n print(f\"[admin] Auto-corrección: eliminadas {deleted} predicciones para {today_str}\")\n\n # 2. Update past results (PENDING -> WIN/LOSS)\n await update_past_results()\n\n # 3. Regenerate fresh predictions\n nba_preds = await generate_nba_predictions()\n football_preds = await generate_football_predictions()\n\n nba_count = len(nba_preds) if nba_preds else 0\n football_count = len(football_preds) if football_preds else 0\n\n return {\n \"status\": \"ok\",\n \"message\": f\"Sistema autocorregido. Eliminadas {deleted} predicciones antiguas. Regeneradas {nba_count} NBA + {football_count} Fútbol.\",\n \"deleted\": deleted,\n \"nba_regenerated\": nba_count,\n \"football_regenerated\": football_count\n }\n except Exception as e:\n print(f\"[admin] Error en force-correct: {e}\")\n raise HTTPException(status_code=500, detail=str(e))\n\n\n@app.get(\"/history/full\")\nasync def get_full_history(background_tasks: BackgroundTasks, days: int = 365):\n \"\"\"Obtiene el historial NBA de los ├║ltimos d├¡as. Dispara actualizaci├│n de marcadores en background.\"\"\"\n # Actualizar partidos pendientes solo si no se ha corrido recientemente (throttle)\n now = datetime.now(timezone.utc)\n if _last_history_update_run is None or (now - _last_history_update_run).total_seconds() > _HISTORY_UPDATE_COOLDOWN_MINUTES * 60:\n background_tasks.add_task(_run_history_update)\n data = history_db.get_history(days)\n # El frontend espera { history: [...] }\n if isinstance(data, list):\n return {\"history\": data}\n return data\n\n\n@app.get(\"/history/football\")\nasync def get_football_history_endpoint(days: int = 30):\n \"\"\"Obtiene el historial de f├║tbol de los ├║ltimos d├¡as.\"\"\"\n import traceback, logging\n logger = logging.getLogger(__name__)\n try:\n data = history_db.get_football_history(days)\n\n if isinstance(data, list):\n try:\n from football_logos import get_team_info\n for p in data:\n home_info = get_team_info(p['home_team'])\n away_info = get_team_info(p['away_team'])\n p['home_team'] = home_info.get('name', p['home_team'])\n p['away_team'] = away_info.get('name', p['away_team'])\n p['home_logo'] = home_info.get('logo')\n p['away_logo'] = away_info.get('logo')\n except Exception as logo_err:\n logger.warning(f\"football_logos enrichment failed (non-fatal): {logo_err}\")\n\n return {\"history\": data}\n return data\n except Exception as e:\n tb = traceback.format_exc()\n logger.error(f\"history/football FAILED:\\n{tb}\")\n return {\"status\": \"error\", \"detail\": str(e), \"traceback\": tb, \"history\": []}\n\n\n# -------------------------------------------\n# DeepSeek API Key management\n# -------------------------------------------\n\n@app.post(\"/api/admin/deepseek-key\")\nasync def save_deepseek_key(request: Request, _: None = Depends(require_admin)):\n \"\"\"Guarda la API key de DeepSeek (solo para esta sesión del servidor).\"\"\"\n try:\n body = await request.json()\n except Exception:\n raise HTTPException(status_code=400, detail=\"Body inválido\")\n key = (body.get(\"key\") or \"\").strip()\n if not key or not key.startswith(\"sk-\"):\n raise HTTPException(status_code=400, detail=\"API key inválida (debe empezar con sk-)\")\n os.environ[\"DEEPSEEK_API_KEY\"] = key\n return {\"status\": \"ok\", \"message\": \"API Key actualizada para esta sesión.\"}\n\n\n# -------------------------------------------\n# News API (DeepSeek agent cache)\n# -------------------------------------------\n\n@app.get(\"/api/news/today\")\nasync def get_today_news():\n \"\"\"Returns all cached news for today's NBA matches.\"\"\"\n date_str = datetime.now(TZ_COLOMBIA).strftime('%Y-%m-%d')\n return {\"date\": date_str, \"news\": history_db.get_news_for_date(date_str)}\n\n\n@app.post(\"/api/news/refresh\")\nasync def refresh_news(background_tasks: BackgroundTasks):\n \"\"\"Busca noticias para los partidos NBA de hoy (local/dev). Requiere DEEPSEEK_API_KEY.\"\"\"\n date_str = datetime.now(TZ_COLOMBIA).strftime('%Y-%m-%d')\n preds = history_db.get_predictions_by_date_light(date_str)\n if not preds:\n return {\"date\": date_str, \"status\": \"skip\", \"message\": \"No hay predicciones NBA para hoy.\"}\n try:\n from news_agent import fetch_news_for_matches\n results = await fetch_news_for_matches(date_str, preds)\n return {\"date\": date_str, \"status\": \"ok\", \"fetched\": len(results), \"news\": results}\n except ValueError as e:\n raise HTTPException(400, str(e))\n except Exception as e:\n raise HTTPException(500, str(e))\n\n\n@app.get(\"/api/news/{match_id:path}\")\nasync def get_match_news(match_id: str):\n \"\"\"Returns cached news for a specific match (NBA or Football).\"\"\"\n data = history_db.get_match_news(match_id)\n if not data:\n return {\"found\": False, \"news\": None}\n return {\"found\": True, \"news\": data}\n\n\n# -------------------------------------------\n# Recommendations API\n# -------------------------------------------\n\n@app.get(\"/api/recommendations/today\")\nasync def get_recommendations_today():\n \"\"\"Returns today's betting recommendations (generates on first call).\"\"\"\n date_str = datetime.now(TZ_COLOMBIA).strftime('%Y-%m-%d')\n try:\n from recommendations import generate_recommendations\n data = await generate_recommendations(date_str)\n return {\"date\": date_str, \"status\": \"ok\", **data}\n except Exception as e:\n print(f\"[main] recommendations error: {e}\")\n return {\"date\": date_str, \"status\": \"error\", \"recommendations\": [], \"parlay_odds\": 0}\n\n\n@app.get(\"/api/recommendations/history\")\nasync def get_recommendations_history():\n \"\"\"Returns past parlay recommendations with win/loss results.\"\"\"\n return {\"history\": history_db.get_recommendations_history(14)}\n\n\n@app.post(\"/api/recommendations/calculate\")\nasync def calculate_parlay_endpoint(request: Request):\n \"\"\"Calculate potential profit for a parlay bet.\n Body: { \"amount\": 50000, \"odds\": [1.85, 2.10, 1.65] }\n \"\"\"\n try:\n body = await request.json()\n except Exception:\n raise HTTPException(status_code=400, detail=\"Body inválido\")\n\n amount = float(body.get(\"amount\", 0))\n odds_list = body.get(\"odds\", [])\n\n if amount <= 0 or not odds_list:\n raise HTTPException(status_code=400, detail=\"Monto y cuotas requeridos\")\n\n from recommendations import calculate_parlay as calc\n result = calc([float(o) for o in odds_list], amount)\n return result\n\n\n# ===========================================\n# STARTUP WEB SERVER\n# ===========================================\nif __name__ == \"__main__\":\n port = int(os.environ.get(\"PORT\", 8000))\n print(f\"Arrancando modo ultraligero en puerto {port}...\")\n uvicorn.run(app, host=\"0.0.0.0\", port=port)\n"}}}}