from flask import Flask, jsonify, request from flask_cors import CORS import json import os import random from src.crawler import Crawler from src.analyzer import ImageAnalyzer from src.monitor import SystemMonitor app = Flask(__name__) CORS(app) REPORT_FILE = 'seo_report.json' # --- Keep Alive Mechanism --- import threading import time import requests class KeepAlive(threading.Thread): def __init__(self, interval=60, target_url="http://127.0.0.1:7860/health"): super().__init__() self.interval = interval self.target_url = target_url self.daemon = True # Stop when main thread stops self.running = True def run(self): print("KeepAlive System Started") while self.running: try: # 1. Log Heartbeat (keeps logs active) print(f"[Heartbeat] System Active - {time.ctime()}") # 2. Self-Ping to keep connection warnings away (Wait for server to start first) time.sleep(self.interval) # Try to self-ping if server is likely up (after 10s) try: requests.get(self.target_url, timeout=5) except: pass # Ignore connection errors during startup/shutdown except Exception as e: print(f"[KeepAlive] Error: {e}") time.sleep(60) class ScheduledCrawler(threading.Thread): def __init__(self, interval=14400): # 14400 seconds = 4 Hours super().__init__() self.interval = interval self.daemon = True self.running = True def run(self): print(f"[Scheduler] Auto-Crawler initialized. Schedule: Every {self.interval/3600} hours.") # Initial delay to let server start time.sleep(60) while self.running: try: # 1. Identify Target # User configures this via Environment Variable target_domain = os.environ.get("AUTO_CRAWL_TARGET") if target_domain: print(f"\n[Scheduler] 🕒 Triggering scheduled crawl for: {target_domain}") # 2. Run Crawl (Reuse logic via internal call or simulating request) # We instantiate the classes directly to avoid network overhead crawler = Crawler() site_data, _, _ = crawler.crawl_domain(target_domain, max_pages=50) # Limit to 50 for auto-runs if site_data: analyzer = ImageAnalyzer() results = analyzer.analyze_site(site_data) # Save Report with open(REPORT_FILE, 'w', encoding='utf-8') as f: json.dump(results, f, indent=4) print(f"[Scheduler] ✅ Crawl finished for {target_domain}. Report saved.") else: print(f"[Scheduler] âš ī¸ Crawl returned no data.") else: print("[Scheduler] â„šī¸ waiting... (Set 'AUTO_CRAWL_TARGET' Env Var to enable auto-crawling)") # 3. Wait for next interval time.sleep(self.interval) except Exception as e: print(f"[Scheduler] Error: {e}") time.sleep(60) # Start KeepAlive & Scheduler pinger = KeepAlive(interval=300) pinger.start() scheduler = ScheduledCrawler(interval=14400) # 4 Hours scheduler.start() # ---------------------------- @app.route('/') def home(): return "Antigravity API is Running. Use /api/status for system info." @app.route('/health') def health_check(): return jsonify({"status": "alive"}), 200 @app.route('/api/status', methods=['GET']) def get_system_status(): """ Returns real-time system metadata including dynamic region and latency. Accepts optional 'domain' parameter to check latency to a specific target. """ domain = request.args.get('domain') stats = SystemMonitor.get_system_stats(target_url=domain) return jsonify(stats) @app.route('/api/seo-report', methods=['GET', 'POST']) def get_seo_report(): # Cache variable attached to function to persist state if not hasattr(get_seo_report, "cache"): get_seo_report.cache = {"data": None, "mtime": 0} def get_cached_report(): if not os.path.exists(REPORT_FILE): return None current_mtime = os.path.getmtime(REPORT_FILE) if get_seo_report.cache["data"] is None or current_mtime > get_seo_report.cache["mtime"]: try: with open(REPORT_FILE, 'r', encoding='utf-8') as f: get_seo_report.cache["data"] = json.load(f) get_seo_report.cache["mtime"] = current_mtime except Exception as e: # If read fails, return None or raise return None return get_seo_report.cache["data"] def get_param(name, default): val = request.args.get(name) or request.form.get(name) if val is None and request.is_json: val = request.json.get(name) return val if val is not None else default # Global Caching for Domain Crawls (Active Memory Cache) if not hasattr(get_seo_report, "domain_cache"): get_seo_report.domain_cache = {} domain = get_param('domain', None) limit = int(get_param('limit', 25)) if domain: # Check Cache (TTL 10 minutes) cache_key = f"{domain}_{limit}" cached_item = get_seo_report.domain_cache.get(cache_key) import time if cached_item: timestamp, data = cached_item # 600 seconds = 10 minutes # DISABLE CACHE TEMPORARILY to ensure fresh code logic is used # if time.time() - timestamp < 600: # print(f"Returning Cached Result for {domain}") # return jsonify(data) try: print(f"Starting live scan for: {domain}") # Initialize Crawler crawler = Crawler() # Crawl the domain with the requested limit site_data, total_discovered, _ = crawler.crawl_domain(domain, max_pages=limit) if not site_data: response = { "summary": { "total_pages_scanned": 0, "total_images_found": 0, "total_images_missing_alt": 0, "total_pages_discovered": 0 }, "details": [] } return jsonify(response) # Analyze Results analyzer = ImageAnalyzer() results = analyzer.analyze_site(site_data) # Add discovery stats results['summary']['total_pages_discovered'] = total_discovered results['details_count'] = len(results['details']) # Save to Cache get_seo_report.domain_cache[cache_key] = (time.time(), results) return jsonify(results) except Exception as e: return jsonify({"error": f"Scraping failed: {str(e)}"}), 500 if not os.path.exists(REPORT_FILE): return jsonify({"error": "Report file not found. Please run result logic first."}), 404 try: data = get_cached_report() if data is None: # Fallback if file doesn't exist or read failed # But if we are here, we passed the os.path.exists check earlier, # so strictly speaking we should just handle the None case. # The previous logic had a check for os.path.exists(REPORT_FILE) at line 59. # We can retain that or rely on get_cached_report returning None. return jsonify({"error": "Report file not found or unreadable."}), 404 # Get parameters for filtering existing report # limit is already extracted above random_param = str(get_param('random', 'true')).lower() is_random = random_param == 'true' summary = data.get('summary', {}) details = data.get('details', []) # Filter details if limit is provided if limit is not None and limit > 0: if is_random and limit < len(details): details = random.sample(details, limit) else: details = details[:limit] response = { "summary": summary, "details_count": len(details), # Useful logic for client "details": details } return jsonify(response) except Exception as e: return jsonify({"error": f"Failed to read report: {str(e)}"}), 500 if __name__ == '__main__': # Run on 0.0.0.0 to be accessible if needed, default port 5000 app.run(debug=True, host='0.0.0.0', port=5050)