diff --git a/src/rf_mapper/web/app.py b/src/rf_mapper/web/app.py index e70f79f..dea5d93 100644 --- a/src/rf_mapper/web/app.py +++ b/src/rf_mapper/web/app.py @@ -1679,12 +1679,21 @@ def create_app(config: Config | None = None) -> Flask: # ==================== Multi-Node Master Dashboard Proxy API ==================== + # Cache for peer responses (scanner_id -> path -> {data, timestamp}) + _peer_cache: dict[str, dict[str, dict]] = {} + _peer_cache_ttl = 300 # 5 minutes cache TTL + def proxy_peer_request(scanner_id: str, path: str, method: str = "GET", **kwargs): - """Proxy a request to a peer node. + """Proxy a request to a peer node with caching. Returns: - Response tuple (jsonify(...), status_code) on error or peer response - None if scanner_id matches local scanner (caller should use local handler) + + Caching behavior: + - GET requests are cached on success + - On peer unreachable, returns cached data if available (with _cached flag) + - POST requests are not cached but will return cached GET data on failure """ local_id = app.config.get("SCANNER_IDENTITY", {}).get("id") if scanner_id == local_id: @@ -1695,10 +1704,13 @@ def create_app(config: Config | None = None) -> Flask: if not peer or not peer.get("url"): return jsonify({"error": "Peer not found"}), 404 + cache_key = f"{scanner_id}:{path}" + try: url = f"{peer['url']}{path}" - resp = requests.request(method, url, timeout=15, **kwargs) + resp = requests.request(method, url, timeout=10, **kwargs) data = resp.json() + # Enrich response with source scanner info data["_source_scanner"] = scanner_id data["_source_position"] = { @@ -1706,9 +1718,39 @@ def create_app(config: Config | None = None) -> Flask: "lon": peer.get("longitude"), "floor": peer.get("floor") } + data["_cached"] = False + + # Cache successful GET responses + if method == "GET": + if scanner_id not in _peer_cache: + _peer_cache[scanner_id] = {} + _peer_cache[scanner_id][path] = { + "data": data, + "timestamp": datetime.now() + } + return jsonify(data) + except requests.RequestException as e: - return jsonify({"error": f"Peer unreachable: {e}"}), 503 + # Try to return cached data + if scanner_id in _peer_cache and path in _peer_cache[scanner_id]: + cached = _peer_cache[scanner_id][path] + cache_age = (datetime.now() - cached["timestamp"]).total_seconds() + + # Return cached data if within TTL (or always for display purposes) + cached_data = cached["data"].copy() + cached_data["_cached"] = True + cached_data["_cached_at"] = cached["timestamp"].isoformat() + cached_data["_cache_age_seconds"] = int(cache_age) + cached_data["_peer_error"] = str(e) + + return jsonify(cached_data) + + return jsonify({ + "error": f"Peer unreachable: {e}", + "_source_scanner": scanner_id, + "_cached": False + }), 503 @app.route("/api/node//latest") def api_node_latest(scanner_id: str): @@ -1742,6 +1784,14 @@ def create_app(config: Config | None = None) -> Flask: return api_trilaterated_positions() return result + @app.route("/api/node//health") + def api_node_health(scanner_id: str): + """Proxy: Get health status from a peer node.""" + result = proxy_peer_request(scanner_id, "/api/health") + if result is None: + return api_health() + return result + return app