From 7d69cac7e7879ff5cef5a47038a97666da367035 Mon Sep 17 00:00:00 2001 From: Smittix Date: Sun, 8 Feb 2026 19:55:00 +0000 Subject: [PATCH] Fix geocoding: validate API responses, clean poisoned cache, improve logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cache lookup now requires non-NULL lat/lon — previously a row with NULL coordinates counted as a cache hit, returning {lat: None, lon: None} which the frontend silently ignored (tower in list but no map pin) - API response handler validates lat/lon exist before caching, preventing error responses (status 200 with error body) from poisoning the cache - On geocoding worker start, delete any existing poisoned cache rows - Geocoding worker now logs "API key not configured" vs "rate limit reached" so the actual problem is visible in logs - API error responses now log the response body for easier debugging --- routes/gsm_spy.py | 22 +++++++++++++++++++--- utils/gsm_geocoding.py | 26 +++++++++++++++++++------- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/routes/gsm_spy.py b/routes/gsm_spy.py index 9086247..90b419f 100644 --- a/routes/gsm_spy.py +++ b/routes/gsm_spy.py @@ -104,6 +104,19 @@ def can_use_api(): def start_geocoding_worker(): """Start background thread for async geocoding.""" global _geocoding_worker_thread + + # Clean poisoned cache entries (rows with NULL lat/lon from failed API responses) + try: + with get_db() as conn: + deleted = conn.execute( + 'DELETE FROM gsm_cells WHERE lat IS NULL OR lon IS NULL' + ).rowcount + conn.commit() + if deleted: + logger.info(f"Cleaned {deleted} poisoned cache entries (NULL coordinates)") + except Exception as e: + logger.warning(f"Could not clean cache: {e}") + if _geocoding_worker_thread is None or not _geocoding_worker_thread.is_alive(): _geocoding_worker_thread = threading.Thread( target=geocoding_worker, @@ -125,10 +138,13 @@ def geocoding_worker(): # Wait for pending tower with timeout tower_data = geocoding_queue.get(timeout=5) - # Check rate limit + # Check API key and rate limit if not can_use_api(): - current_usage = get_api_usage_today() - logger.warning(f"OpenCellID API rate limit reached ({current_usage}/{config.GSM_API_DAILY_LIMIT})") + if not config.GSM_OPENCELLID_API_KEY: + logger.warning("OpenCellID API key not configured (set INTERCEPT_GSM_OPENCELLID_API_KEY)") + else: + current_usage = get_api_usage_today() + logger.warning(f"OpenCellID API daily limit reached ({current_usage}/{config.GSM_API_DAILY_LIMIT})") geocoding_queue.task_done() continue diff --git a/utils/gsm_geocoding.py b/utils/gsm_geocoding.py index 681b990..006368d 100644 --- a/utils/gsm_geocoding.py +++ b/utils/gsm_geocoding.py @@ -47,7 +47,7 @@ def lookup_cell_coordinates(mcc: int, mnc: int, lac: int, cid: int) -> dict[str, WHERE mcc = ? AND mnc = ? AND lac = ? AND cid = ? ''', (mcc, mnc, lac, cid)).fetchone() - if result: + if result and result['lat'] is not None and result['lon'] is not None: return { 'lat': result['lat'], 'lon': result['lon'], @@ -95,6 +95,16 @@ def lookup_cell_from_api(mcc: int, mnc: int, lac: int, cid: int) -> dict[str, An if response.status_code == 200: cell_data = response.json() + lat = cell_data.get('lat') + lon = cell_data.get('lon') + + # Validate response has actual coordinates + if lat is None or lon is None: + logger.warning( + f"OpenCellID API returned 200 but no coordinates for " + f"MCC={mcc} MNC={mnc} LAC={lac} CID={cid}: {cell_data}" + ) + return None # Cache the result with get_db() as conn: @@ -104,8 +114,7 @@ def lookup_cell_from_api(mcc: int, mnc: int, lac: int, cid: int) -> dict[str, An VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) ''', ( mcc, mnc, lac, cid, - cell_data.get('lat'), - cell_data.get('lon'), + lat, lon, cell_data.get('azimuth'), cell_data.get('range'), cell_data.get('samples'), @@ -114,11 +123,11 @@ def lookup_cell_from_api(mcc: int, mnc: int, lac: int, cid: int) -> dict[str, An )) conn.commit() - logger.info(f"Cached cell tower from API: MCC={mcc} MNC={mnc} LAC={lac} CID={cid}") + logger.info(f"Cached cell tower from API: MCC={mcc} MNC={mnc} LAC={lac} CID={cid} -> ({lat}, {lon})") return { - 'lat': cell_data.get('lat'), - 'lon': cell_data.get('lon'), + 'lat': lat, + 'lon': lon, 'source': 'api', 'azimuth': cell_data.get('azimuth'), 'range_meters': cell_data.get('range'), @@ -126,7 +135,10 @@ def lookup_cell_from_api(mcc: int, mnc: int, lac: int, cid: int) -> dict[str, An 'radio': cell_data.get('radio') } else: - logger.warning(f"OpenCellID API returned {response.status_code} for MCC={mcc} MNC={mnc} LAC={lac} CID={cid}") + logger.warning( + f"OpenCellID API returned {response.status_code} for " + f"MCC={mcc} MNC={mnc} LAC={lac} CID={cid}: {response.text[:200]}" + ) return None except Exception as e: