Fix geocoding: validate API responses, clean poisoned cache, improve logging

- 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
This commit is contained in:
Smittix
2026-02-08 19:55:00 +00:00
parent c6a8a4a492
commit 7d69cac7e7
2 changed files with 38 additions and 10 deletions
+19 -3
View File
@@ -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
+19 -7
View File
@@ -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: