diff --git a/web/src/padelnomics/admin/routes.py b/web/src/padelnomics/admin/routes.py index a819294..ad3f133 100644 --- a/web/src/padelnomics/admin/routes.py +++ b/web/src/padelnomics/admin/routes.py @@ -2,6 +2,7 @@ Admin domain: role-based admin panel for managing users, tasks, etc. """ import json +import logging from datetime import date, timedelta from pathlib import Path @@ -33,6 +34,8 @@ from ..core import ( utcnow_iso, ) +logger = logging.getLogger(__name__) + # Blueprint with its own template folder bp = Blueprint( "admin", @@ -50,6 +53,7 @@ async def _inject_admin_sidebar_data(): row = await fetch_one("SELECT COUNT(*) as cnt FROM inbound_emails WHERE is_read = 0") g.admin_unread_count = row["cnt"] if row else 0 except Exception: + logger.exception("Failed to load admin sidebar unread count") g.admin_unread_count = 0 @@ -1041,7 +1045,7 @@ async def email_detail(email_id: int): else: enriched_html = getattr(result, "html", "") except Exception: - pass # Metadata-only fallback + logger.warning("Failed to fetch email body from Resend for %s", email["resend_id"], exc_info=True) return await render_template( "admin/email_detail.html", @@ -1194,7 +1198,7 @@ async def audiences(): data = getattr(contacts, "data", []) a["contact_count"] = len(data) if data else 0 except Exception: - pass + logger.warning("Failed to fetch contact count for audience %s", a.get("audience_id"), exc_info=True) return await render_template("admin/audiences.html", audiences=audience_list) @@ -1220,6 +1224,7 @@ async def audience_contacts(audience_id: str): else: contacts = getattr(result, "data", []) or [] except Exception: + logger.exception("Failed to fetch contacts from Resend for audience %s", audience_id) await flash("Failed to fetch contacts from Resend.", "error") return await render_template( diff --git a/web/src/padelnomics/analytics.py b/web/src/padelnomics/analytics.py index 5513ca2..4d7e432 100644 --- a/web/src/padelnomics/analytics.py +++ b/web/src/padelnomics/analytics.py @@ -10,10 +10,13 @@ Usage: rows = await fetch_analytics("SELECT * FROM serving.planner_defaults WHERE city_slug = ?", ["berlin"]) """ import asyncio +import logging import os from pathlib import Path from typing import Any +logger = logging.getLogger(__name__) + _conn = None # duckdb.DuckDBPyConnection | None — lazy import _DUCKDB_PATH = os.environ.get("SERVING_DUCKDB_PATH", "data/analytics.duckdb") @@ -43,7 +46,7 @@ async def fetch_analytics(sql: str, params: list | None = None) -> list[dict[str Run a read-only DuckDB query and return rows as dicts. Returns [] if analytics DB is unavailable (not yet built, or DUCKDB_PATH unset). - Never raises — callers should treat empty results as "no data yet". + Returns [] on query error after logging — callers treat empty results as "no data yet". """ assert sql, "sql must not be empty" @@ -62,4 +65,5 @@ async def fetch_analytics(sql: str, params: list | None = None) -> list[dict[str try: return await asyncio.to_thread(_run) except Exception: + logger.exception("DuckDB analytics query failed: %.200s", sql) return [] diff --git a/web/src/padelnomics/core.py b/web/src/padelnomics/core.py index ccf41e5..76a5f08 100644 --- a/web/src/padelnomics/core.py +++ b/web/src/padelnomics/core.py @@ -455,6 +455,7 @@ async def _get_or_create_resend_audience(name: str) -> str | None: ) return audience_id except Exception: + logger.exception("Failed to create Resend audience %r", name) return None @@ -495,7 +496,8 @@ async def capture_waitlist_email( ) is_new = cursor_result > 0 except Exception: - # If anything fails, treat as not-new to avoid double-sending + logger.exception("Failed to insert waitlist entry for %r", email) + # Treat as not-new to avoid double-sending on retry is_new = False # Enqueue confirmation email only if new @@ -517,7 +519,8 @@ async def capture_waitlist_email( resend.api_key = config.RESEND_API_KEY resend.Contacts.create({"email": email, "audience_id": audience_id}) except Exception: - pass # Silent fail + # Non-critical: audience sync failure doesn't affect waitlist capture + logger.warning("Failed to add %r to Resend audience %r", email, audience_name, exc_info=True) return is_new