"""VNEWS v2 Patch - auto scheduler + status endpoints + keep-alive. This is imported by app_v2_entry.py to add auto posting functionality. FIX v2: Catch-up scheduler + keep-alive to prevent Space sleep """ import sys, os, threading, json, time, logging from datetime import datetime, timezone, timedelta from fastapi import Request from fastapi.responses import JSONResponse import requests as _req VN_TZ = timezone(timedelta(hours=7)) LOG = logging.getLogger("app_v2_patch") LOG.setLevel(logging.INFO) if not LOG.handlers: ch = logging.StreamHandler() ch.setFormatter(logging.Formatter('%(asctime)s [app_v2_patch] %(levelname)s: %(message)s')) LOG.addHandler(ch) # ===== Keep-alive: prevent Space from sleeping ===== # HF Spaces sleep after ~30 min of inactivity on free tier # This thread pings the Space every 10 minutes to keep it alive SPACE_URL = "https://bep40-vnews.hf.space" def _keep_alive_loop(): """Ping the Space every 10 minutes to prevent sleep.""" LOG.info(f"🔄 Keep-alive thread started - ping {SPACE_URL} every 10 min") while True: try: time.sleep(600) # 10 minutes _req.get(f"{SPACE_URL}/api/scheduler/status", headers={"User-Agent": "VNEWS-KeepAlive/1.0"}, timeout=15) LOG.debug("Keep-alive ping OK") except Exception as e: LOG.warning(f"Keep-alive ping failed (Space may be sleeping): {e}") # Start keep-alive in background try: _ka_thread = threading.Thread(target=_keep_alive_loop, daemon=True, name="keep-alive") _ka_thread.start() LOG.info("🔄 Keep-alive started - Space will stay awake") except Exception as e: LOG.warning(f"Keep-alive start failed: {e}") # ===== Start auto scheduler ===== try: import auto_scheduler as _as _as.start_auto_scheduler() LOG.info("[auto_scheduler] Started successfully - will post at 7:00, 13:00, 19:00 VN time (with catch-up)") except Exception as e: LOG.error(f"[auto_scheduler] Start failed: {e}") def register_scheduler_endpoints(app): """Register scheduler status/trigger endpoints on the FastAPI app.""" @app.get('/api/scheduler/status') def scheduler_status(): running = any(t.name == 'auto-scheduler' and t.is_alive() for t in threading.enumerate()) keep_alive = any(t.name == 'keep-alive' and t.is_alive() for t in threading.enumerate()) # Load state to show which slots ran today today_str = datetime.now(VN_TZ).strftime('%Y-%m-%d') state = {} try: state_file = '/data/scheduler_state.json' if os.path.isdir('/data') else None if state_file and os.path.exists(state_file): state = json.load(open(state_file, 'r')) except: pass ran_today = state.get(today_str, {}) if state else {} return JSONResponse({ "running": running, "keep_alive": keep_alive, "schedule": "7:00, 13:00, 19:00 VN time", "today": today_str, "slots_ran_today": ran_today, "catch_up_enabled": True, "next_run": "7:00, 13:00, or 19:00 VN time (whichever is next)" }) @app.post('/api/scheduler/trigger') async def scheduler_trigger(): try: import auto_scheduler as _as2 _as2._run_scheduled_posting() return JSONResponse({"ok": True, "message": "Scheduled posting triggered manually"}) except Exception as e: return JSONResponse({"ok": False, "error": str(e)}, status_code=500) @app.get('/api/scheduler/force') def scheduler_force(): """Force-run all missed slots immediately. Useful after deploy.""" try: import auto_scheduler as _as2 _as2._check_missed_slots() return JSONResponse({"ok": True, "message": "Missed slots check triggered"}) except Exception as e: return JSONResponse({"ok": False, "error": str(e)}, status_code=500) return app # Auto-register on the main app from app_v2_entry try: from main import app register_scheduler_endpoints(app) LOG.info("[app_v2_patch] Scheduler endpoints registered: /api/scheduler/status, /api/scheduler/trigger, /api/scheduler/force") except Exception as e: LOG.error(f"[app_v2_patch] Could not register endpoints: {e}")