diff --git a/docker-compose.yml b/docker-compose.yml index d98170b7..648a6380 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,7 +39,7 @@ services: container_name: polyweather_prewarm restart: unless-stopped profiles: ["workers"] - command: python scripts/prewarm_dashboard_worker.py --include-detail --include-market --force-refresh + command: python scripts/prewarm_dashboard_worker.py --include-detail --include-market volumes: - ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather - ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data diff --git a/src/database/db_manager.py b/src/database/db_manager.py index 28447e66..cfc86079 100644 --- a/src/database/db_manager.py +++ b/src/database/db_manager.py @@ -42,6 +42,75 @@ class DBManager: "Prefer": "return=minimal", } + def _supabase_admin_users_endpoint(self) -> str: + supabase_url = str(os.getenv("SUPABASE_URL") or "").strip().rstrip("/") + if not supabase_url: + return "" + return f"{supabase_url}/auth/v1/admin/users" + + def _sync_points_to_supabase_user_metadata(self, telegram_id: int) -> bool: + supabase_url = str(os.getenv("SUPABASE_URL") or "").strip().rstrip("/") + if not supabase_url: + return False + headers = self._supabase_service_headers() + if not headers: + return False + endpoint = self._supabase_admin_users_endpoint() + if not endpoint: + return False + + supabase_user_id = None + points = 0 + with self._get_connection() as conn: + conn.row_factory = sqlite3.Row + row = conn.execute( + "SELECT supabase_user_id FROM supabase_bindings WHERE telegram_id = ? LIMIT 1", + (int(telegram_id),), + ).fetchone() + if row and row["supabase_user_id"]: + supabase_user_id = str(row["supabase_user_id"]).strip() + if not supabase_user_id: + row = conn.execute( + "SELECT supabase_user_id FROM users WHERE telegram_id = ? LIMIT 1", + (int(telegram_id),), + ).fetchone() + if row and row["supabase_user_id"]: + supabase_user_id = str(row["supabase_user_id"]).strip() + if not supabase_user_id: + return False + pts_row = conn.execute( + "SELECT points FROM users WHERE telegram_id = ? LIMIT 1", + (int(telegram_id),), + ).fetchone() + if pts_row: + points = max(0, int(pts_row["points"] or 0)) + + try: + resp = requests.patch( + f"{endpoint}/{supabase_user_id}", + json={"user_metadata": {"points": points}}, + headers={**headers, "Prefer": "return=minimal"}, + timeout=8, + ) + if resp.status_code not in (200, 204): + logger.warning( + "supabase points sync failed tg={} suid={} status={} body={}", + telegram_id, + supabase_user_id, + resp.status_code, + (resp.text or "")[:200], + ) + return False + return True + except Exception as exc: + logger.warning( + "supabase points sync error tg={} suid={}: {}", + telegram_id, + supabase_user_id, + exc, + ) + return False + def _sync_supabase_profile_telegram_fields( self, *, @@ -1010,6 +1079,36 @@ class DBManager: except Exception: return 0 + def get_points_by_supabase_email(self, supabase_email: str) -> int: + email = str(supabase_email or "").strip().lower() + if not email: + return 0 + with self._get_connection() as conn: + conn.row_factory = sqlite3.Row + row = conn.execute( + """ + SELECT points + FROM users + WHERE lower(trim(COALESCE(supabase_email, ''))) = ? + LIMIT 1 + """, + (email,), + ).fetchone() + if not row: + row = conn.execute( + """ + SELECT u.points + FROM users u + JOIN supabase_bindings b ON b.telegram_id = u.telegram_id + WHERE lower(trim(COALESCE(b.supabase_email, ''))) = ? + LIMIT 1 + """, + (email,), + ).fetchone() + if row: + return max(0, int(row["points"] or 0)) + return 0 + def grant_points_by_supabase_email( self, supabase_email: str, @@ -1048,6 +1147,7 @@ class DBManager: (after, telegram_id), ) conn.commit() + self._sync_points_to_supabase_user_metadata(telegram_id) return { "ok": True, "telegram_id": telegram_id, @@ -1421,6 +1521,7 @@ class DBManager: points=weekly_points + total_added, ) conn.commit() + self._sync_points_to_supabase_user_metadata(telegram_id) return { "awarded": True, "reason": "ok", @@ -1492,6 +1593,7 @@ class DBManager: (new_balance, telegram_id), ) conn.commit() + self._sync_points_to_supabase_user_metadata(telegram_id) return {"ok": True, "balance": new_balance, "spent": amount} def spend_points_by_supabase_user_id(self, supabase_user_id: str, amount: int) -> Dict[str, Any]: @@ -1529,6 +1631,7 @@ class DBManager: (new_balance, telegram_id), ) conn.commit() + self._sync_points_to_supabase_user_metadata(telegram_id) return {"ok": True, "balance": new_balance, "spent": amount} def set_premium(self, telegram_id: int, plan: str, months: int = 1): @@ -1839,6 +1942,8 @@ class DBManager: ), ) conn.commit() + if bonus > 0: + self._sync_points_to_supabase_user_metadata(int(telegram_id)) return True def append_airport_obs( diff --git a/web/app.py b/web/app.py index 070a026b..dbdacc31 100644 --- a/web/app.py +++ b/web/app.py @@ -35,4 +35,9 @@ __all__ = [ if __name__ == "__main__": import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8000) + uvicorn.run( + "web.app:app", + host="0.0.0.0", + port=8000, + workers=int(os.getenv("UVICORN_WORKERS", "4")), + ) diff --git a/web/core.py b/web/core.py index 742008af..16899218 100644 --- a/web/core.py +++ b/web/core.py @@ -239,19 +239,29 @@ def _resolve_auth_points(request: Request) -> int: points = max(0, int(raw_points or 0)) except Exception: points = 0 - if points > 0: - return points user_id = str(getattr(request.state, "auth_user_id", "") or "").strip() - if not user_id: - return points - try: - db_points = _account_db.get_points_by_supabase_user_id(user_id) - if db_points > points: - request.state.auth_points = db_points - return db_points - except Exception as exc: - logger.warning(f"auth points fallback failed user_id={user_id}: {exc}") + + if user_id: + try: + db_points = _account_db.get_points_by_supabase_user_id(user_id) + if db_points > points: + request.state.auth_points = db_points + points = db_points + except Exception as exc: + logger.warning(f"auth points fallback failed user_id={user_id}: {exc}") + + if points <= 0: + email = str(getattr(request.state, "auth_email", "") or "").strip().lower() + if email: + try: + email_points = _account_db.get_points_by_supabase_email(email) + if email_points > points: + request.state.auth_points = email_points + points = email_points + except Exception as exc: + logger.warning(f"auth points email fallback failed email={email}: {exc}") + return points