移除 Polymarket 价格拉取:删除 _market_layer、market_scan 返回空、清理健康检查和配置验证
This commit is contained in:
@@ -71,7 +71,6 @@ def validate_runtime_env(
|
|||||||
entitlement_guard = _env_bool("POLYWEATHER_REQUIRE_ENTITLEMENT", False)
|
entitlement_guard = _env_bool("POLYWEATHER_REQUIRE_ENTITLEMENT", False)
|
||||||
payment_enabled = _env_bool("POLYWEATHER_PAYMENT_ENABLED", False)
|
payment_enabled = _env_bool("POLYWEATHER_PAYMENT_ENABLED", False)
|
||||||
weekly_reward_enabled = _env_bool("POLYWEATHER_WEEKLY_REWARD_ENABLED", False)
|
weekly_reward_enabled = _env_bool("POLYWEATHER_WEEKLY_REWARD_ENABLED", False)
|
||||||
wallet_activity_enabled = _env_bool("POLYMARKET_WALLET_ACTIVITY_ENABLED", False)
|
|
||||||
polygon_watch_enabled = _env_bool("POLYGON_WALLET_WATCH_ENABLED", False)
|
polygon_watch_enabled = _env_bool("POLYGON_WALLET_WATCH_ENABLED", False)
|
||||||
|
|
||||||
if component_key == "bot":
|
if component_key == "bot":
|
||||||
@@ -79,7 +78,9 @@ def validate_runtime_env(
|
|||||||
if missing:
|
if missing:
|
||||||
report.errors.append(f"Bot 启动缺少必填变量: {', '.join(missing)}")
|
report.errors.append(f"Bot 启动缺少必填变量: {', '.join(missing)}")
|
||||||
if not (_has("TELEGRAM_CHAT_ID") or _has("TELEGRAM_CHAT_IDS")):
|
if not (_has("TELEGRAM_CHAT_ID") or _has("TELEGRAM_CHAT_IDS")):
|
||||||
report.warnings.append("未配置 TELEGRAM_CHAT_ID / TELEGRAM_CHAT_IDS,机器人推送目标为空")
|
report.warnings.append(
|
||||||
|
"未配置 TELEGRAM_CHAT_ID / TELEGRAM_CHAT_IDS,机器人推送目标为空"
|
||||||
|
)
|
||||||
|
|
||||||
if auth_enabled:
|
if auth_enabled:
|
||||||
missing = _missing(["SUPABASE_URL", "SUPABASE_ANON_KEY"])
|
missing = _missing(["SUPABASE_URL", "SUPABASE_ANON_KEY"])
|
||||||
@@ -93,12 +94,16 @@ def validate_runtime_env(
|
|||||||
if entitlement_guard:
|
if entitlement_guard:
|
||||||
missing = _missing(["POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN"])
|
missing = _missing(["POLYWEATHER_BACKEND_ENTITLEMENT_TOKEN"])
|
||||||
if missing:
|
if missing:
|
||||||
report.errors.append(f"已启用 backend entitlement guard,但缺少变量: {', '.join(missing)}")
|
report.errors.append(
|
||||||
|
f"已启用 backend entitlement guard,但缺少变量: {', '.join(missing)}"
|
||||||
|
)
|
||||||
|
|
||||||
if payment_enabled:
|
if payment_enabled:
|
||||||
payment_missing = _missing(["POLYWEATHER_PAYMENT_RPC_URL"])
|
payment_missing = _missing(["POLYWEATHER_PAYMENT_RPC_URL"])
|
||||||
if payment_missing:
|
if payment_missing:
|
||||||
report.errors.append(f"已启用支付,但缺少变量: {', '.join(payment_missing)}")
|
report.errors.append(
|
||||||
|
f"已启用支付,但缺少变量: {', '.join(payment_missing)}"
|
||||||
|
)
|
||||||
has_receiver = _has("POLYWEATHER_PAYMENT_RECEIVER_CONTRACT")
|
has_receiver = _has("POLYWEATHER_PAYMENT_RECEIVER_CONTRACT")
|
||||||
has_tokens_json = _has("POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON")
|
has_tokens_json = _has("POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON")
|
||||||
if not (has_receiver or has_tokens_json):
|
if not (has_receiver or has_tokens_json):
|
||||||
@@ -106,13 +111,11 @@ def validate_runtime_env(
|
|||||||
"已启用支付,但未配置 POLYWEATHER_PAYMENT_RECEIVER_CONTRACT 或 POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON"
|
"已启用支付,但未配置 POLYWEATHER_PAYMENT_RECEIVER_CONTRACT 或 POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON"
|
||||||
)
|
)
|
||||||
|
|
||||||
if wallet_activity_enabled:
|
|
||||||
if not _has("POLYMARKET_WALLET_ACTIVITY_USERS"):
|
|
||||||
report.warnings.append("已启用 wallet activity watcher,但未配置 POLYMARKET_WALLET_ACTIVITY_USERS")
|
|
||||||
|
|
||||||
if polygon_watch_enabled:
|
if polygon_watch_enabled:
|
||||||
if not _has("POLYGON_WALLET_WATCH_ADDRESSES"):
|
if not _has("POLYGON_WALLET_WATCH_ADDRESSES"):
|
||||||
report.warnings.append("已启用 polygon watcher,但未配置 POLYGON_WALLET_WATCH_ADDRESSES")
|
report.warnings.append(
|
||||||
|
"已启用 polygon watcher,但未配置 POLYGON_WALLET_WATCH_ADDRESSES"
|
||||||
|
)
|
||||||
|
|
||||||
if component_key == "web":
|
if component_key == "web":
|
||||||
if auth_enabled and not auth_required:
|
if auth_enabled and not auth_required:
|
||||||
|
|||||||
@@ -40,27 +40,6 @@ def get_telegram_chat_ids_from_env() -> List[str]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_polymarket_wallet_activity_chat_ids_from_env() -> List[str]:
|
|
||||||
"""
|
|
||||||
Preferred envs for wallet activity push:
|
|
||||||
- POLYMARKET_WALLET_ACTIVITY_CHAT_IDS (comma-separated)
|
|
||||||
- POLYMARKET_WALLET_ACTIVITY_CHAT_ID (single)
|
|
||||||
|
|
||||||
Falls back to global TELEGRAM_CHAT_IDS / TELEGRAM_CHAT_ID for compatibility.
|
|
||||||
"""
|
|
||||||
dedicated = parse_telegram_chat_ids(
|
|
||||||
os.getenv("POLYMARKET_WALLET_ACTIVITY_CHAT_IDS"),
|
|
||||||
os.getenv("POLYMARKET_WALLET_ACTIVITY_CHAT_ID"),
|
|
||||||
)
|
|
||||||
if dedicated:
|
|
||||||
return dedicated
|
|
||||||
return parse_telegram_chat_ids(
|
|
||||||
os.getenv("TELEGRAM_CHAT_IDS"),
|
|
||||||
os.getenv("TELEGRAM_CHAT_ID"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_primary_telegram_chat_id_from_env() -> str:
|
def get_primary_telegram_chat_id_from_env() -> str:
|
||||||
chat_ids = get_telegram_chat_ids_from_env()
|
chat_ids = get_telegram_chat_ids_from_env()
|
||||||
return chat_ids[0] if chat_ids else ""
|
return chat_ids[0] if chat_ids else ""
|
||||||
|
|||||||
+59
-23
@@ -19,7 +19,6 @@ from src.utils.config_validation import validate_runtime_env
|
|||||||
from src.data_collection.weather_sources import WeatherDataCollector
|
from src.data_collection.weather_sources import WeatherDataCollector
|
||||||
from src.data_collection.country_networks import provider_coverage_summary
|
from src.data_collection.country_networks import provider_coverage_summary
|
||||||
from src.data_collection.city_risk_profiles import CITY_RISK_PROFILES # noqa: F401
|
from src.data_collection.city_risk_profiles import CITY_RISK_PROFILES # noqa: F401
|
||||||
from src.data_collection.polymarket_readonly import PolymarketReadOnlyLayer
|
|
||||||
from src.auth.supabase_entitlement import SUPABASE_ENTITLEMENT, extract_bearer_token
|
from src.auth.supabase_entitlement import SUPABASE_ENTITLEMENT, extract_bearer_token
|
||||||
from src.utils.metrics import (
|
from src.utils.metrics import (
|
||||||
build_metrics_summary,
|
build_metrics_summary,
|
||||||
@@ -53,7 +52,6 @@ for _warning in _config_validation.warnings:
|
|||||||
if _config_validation.errors:
|
if _config_validation.errors:
|
||||||
raise RuntimeError(" | ".join(_config_validation.errors))
|
raise RuntimeError(" | ".join(_config_validation.errors))
|
||||||
_weather = WeatherDataCollector(_config)
|
_weather = WeatherDataCollector(_config)
|
||||||
_market_layer = PolymarketReadOnlyLayer()
|
|
||||||
_account_db = DBManager()
|
_account_db = DBManager()
|
||||||
|
|
||||||
CITIES: Dict[str, Dict[str, Any]] = {
|
CITIES: Dict[str, Dict[str, Any]] = {
|
||||||
@@ -62,7 +60,10 @@ CITIES: Dict[str, Dict[str, Any]] = {
|
|||||||
"lon": info["lon"],
|
"lon": info["lon"],
|
||||||
"f": info["use_fahrenheit"],
|
"f": info["use_fahrenheit"],
|
||||||
"tz": info["tz_offset"],
|
"tz": info["tz_offset"],
|
||||||
"settlement_source": str(info.get("settlement_source") or "metar").strip().lower() or "metar",
|
"settlement_source": str(info.get("settlement_source") or "metar")
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
or "metar",
|
||||||
}
|
}
|
||||||
for cid, info in CITY_REGISTRY.items()
|
for cid, info in CITY_REGISTRY.items()
|
||||||
}
|
}
|
||||||
@@ -141,6 +142,7 @@ async def _etag_middleware(request: Request, call_next):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
etag = hashlib.md5(body).hexdigest()
|
etag = hashlib.md5(body).hexdigest()
|
||||||
except Exception:
|
except Exception:
|
||||||
return response
|
return response
|
||||||
@@ -149,12 +151,14 @@ async def _etag_middleware(request: Request, call_next):
|
|||||||
if_none_match = request.headers.get("If-None-Match", "")
|
if_none_match = request.headers.get("If-None-Match", "")
|
||||||
if if_none_match == etag_value:
|
if if_none_match == etag_value:
|
||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
|
|
||||||
return Response(status_code=304, headers={"ETag": etag_value})
|
return Response(status_code=304, headers={"ETag": etag_value})
|
||||||
|
|
||||||
response.headers["ETag"] = etag_value
|
response.headers["ETag"] = etag_value
|
||||||
response.headers["Cache-Control"] = "private, max-age=30"
|
response.headers["Cache-Control"] = "private, max-age=30"
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def _env_bool(name: str, default: bool = False) -> bool:
|
def _env_bool(name: str, default: bool = False) -> bool:
|
||||||
raw = os.getenv(name)
|
raw = os.getenv(name)
|
||||||
if raw is None:
|
if raw is None:
|
||||||
@@ -227,7 +231,9 @@ def _resolve_auth_points(request: Request) -> int:
|
|||||||
request.state.auth_points = email_points
|
request.state.auth_points = email_points
|
||||||
points = email_points
|
points = email_points
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning(f"auth points email fallback failed email={email}: {exc}")
|
logger.warning(
|
||||||
|
f"auth points email fallback failed email={email}: {exc}"
|
||||||
|
)
|
||||||
|
|
||||||
return points
|
return points
|
||||||
|
|
||||||
@@ -289,9 +295,14 @@ def _assert_entitlement(request: Request) -> None:
|
|||||||
|
|
||||||
def _require_supabase_identity(request: Request) -> Dict[str, str]:
|
def _require_supabase_identity(request: Request) -> Dict[str, str]:
|
||||||
if not SUPABASE_ENTITLEMENT.enabled:
|
if not SUPABASE_ENTITLEMENT.enabled:
|
||||||
raise HTTPException(status_code=503, detail="payment requires POLYWEATHER_AUTH_ENABLED=true")
|
raise HTTPException(
|
||||||
|
status_code=503, detail="payment requires POLYWEATHER_AUTH_ENABLED=true"
|
||||||
|
)
|
||||||
if not SUPABASE_ENTITLEMENT.configured:
|
if not SUPABASE_ENTITLEMENT.configured:
|
||||||
raise HTTPException(status_code=503, detail="payment requires SUPABASE_URL and SUPABASE_ANON_KEY")
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail="payment requires SUPABASE_URL and SUPABASE_ANON_KEY",
|
||||||
|
)
|
||||||
|
|
||||||
state_user_id = str(getattr(request.state, "auth_user_id", "") or "").strip()
|
state_user_id = str(getattr(request.state, "auth_user_id", "") or "").strip()
|
||||||
if state_user_id:
|
if state_user_id:
|
||||||
@@ -306,9 +317,13 @@ def _require_supabase_identity(request: Request) -> Dict[str, str]:
|
|||||||
|
|
||||||
legacy_ok = _legacy_service_token_valid(request)
|
legacy_ok = _legacy_service_token_valid(request)
|
||||||
if legacy_ok:
|
if legacy_ok:
|
||||||
forwarded_user_id = str(request.headers.get(_FORWARDED_SUPABASE_USER_ID_HEADER) or "").strip()
|
forwarded_user_id = str(
|
||||||
|
request.headers.get(_FORWARDED_SUPABASE_USER_ID_HEADER) or ""
|
||||||
|
).strip()
|
||||||
if forwarded_user_id:
|
if forwarded_user_id:
|
||||||
forwarded_email = str(request.headers.get(_FORWARDED_SUPABASE_EMAIL_HEADER) or "").strip()
|
forwarded_email = str(
|
||||||
|
request.headers.get(_FORWARDED_SUPABASE_EMAIL_HEADER) or ""
|
||||||
|
).strip()
|
||||||
return {"user_id": forwarded_user_id, "email": forwarded_email}
|
return {"user_id": forwarded_user_id, "email": forwarded_email}
|
||||||
# Entitlement token is valid but forwarded headers are missing.
|
# Entitlement token is valid but forwarded headers are missing.
|
||||||
# Return a placeholder identity — callers (e.g. _require_ops_admin)
|
# Return a placeholder identity — callers (e.g. _require_ops_admin)
|
||||||
@@ -316,12 +331,15 @@ def _require_supabase_identity(request: Request) -> Dict[str, str]:
|
|||||||
return {"user_id": "entitlement", "email": ""}
|
return {"user_id": "entitlement", "email": ""}
|
||||||
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"payment auth identity missing state_user={} auth_bearer={} legacy_ok={} forwarded_user={}"
|
"payment auth identity missing state_user={} auth_bearer={} legacy_ok={} forwarded_user={}".format(
|
||||||
.format(
|
|
||||||
bool(state_user_id),
|
bool(state_user_id),
|
||||||
bool(token),
|
bool(token),
|
||||||
bool(legacy_ok),
|
bool(legacy_ok),
|
||||||
bool(str(request.headers.get(_FORWARDED_SUPABASE_USER_ID_HEADER) or "").strip()),
|
bool(
|
||||||
|
str(
|
||||||
|
request.headers.get(_FORWARDED_SUPABASE_USER_ID_HEADER) or ""
|
||||||
|
).strip()
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
@@ -342,7 +360,10 @@ def _require_ops_admin(request: Request) -> Dict[str, str]:
|
|||||||
# missing forwarded headers), loosen the requirement: entitlement token
|
# missing forwarded headers), loosen the requirement: entitlement token
|
||||||
# alone is sufficient admin proof when admin list is configured.
|
# alone is sufficient admin proof when admin list is configured.
|
||||||
if is_entitlement:
|
if is_entitlement:
|
||||||
granted = {"user_id": "admin:entitlement", "email": next(iter(_OPS_ADMIN_EMAILS))}
|
granted = {
|
||||||
|
"user_id": "admin:entitlement",
|
||||||
|
"email": next(iter(_OPS_ADMIN_EMAILS)),
|
||||||
|
}
|
||||||
return granted
|
return granted
|
||||||
raise HTTPException(status_code=403, detail="ops admin required")
|
raise HTTPException(status_code=403, detail="ops admin required")
|
||||||
|
|
||||||
@@ -437,15 +458,24 @@ def _cache_summary() -> Dict[str, Any]:
|
|||||||
|
|
||||||
open_meteo_forecast_entries = len(getattr(_weather, "_open_meteo_cache", {}) or {})
|
open_meteo_forecast_entries = len(getattr(_weather, "_open_meteo_cache", {}) or {})
|
||||||
open_meteo_ensemble_entries = len(getattr(_weather, "_ensemble_cache", {}) or {})
|
open_meteo_ensemble_entries = len(getattr(_weather, "_ensemble_cache", {}) or {})
|
||||||
open_meteo_multi_model_entries = len(getattr(_weather, "_multi_model_cache", {}) or {})
|
open_meteo_multi_model_entries = len(
|
||||||
|
getattr(_weather, "_multi_model_cache", {}) or {}
|
||||||
|
)
|
||||||
metar_entries = len(getattr(_weather, "_metar_cache", {}) or {})
|
metar_entries = len(getattr(_weather, "_metar_cache", {}) or {})
|
||||||
taf_entries = len(getattr(_weather, "_taf_cache", {}) or {})
|
taf_entries = len(getattr(_weather, "_taf_cache", {}) or {})
|
||||||
settlement_entries = len(getattr(_weather, "_settlement_cache", {}) or {})
|
settlement_entries = len(getattr(_weather, "_settlement_cache", {}) or {})
|
||||||
|
|
||||||
gauge_set("polyweather_api_cache_entries", len(_cache))
|
gauge_set("polyweather_api_cache_entries", len(_cache))
|
||||||
gauge_set("polyweather_open_meteo_forecast_cache_entries", open_meteo_forecast_entries)
|
gauge_set(
|
||||||
gauge_set("polyweather_open_meteo_ensemble_cache_entries", open_meteo_ensemble_entries)
|
"polyweather_open_meteo_forecast_cache_entries", open_meteo_forecast_entries
|
||||||
gauge_set("polyweather_open_meteo_multi_model_cache_entries", open_meteo_multi_model_entries)
|
)
|
||||||
|
gauge_set(
|
||||||
|
"polyweather_open_meteo_ensemble_cache_entries", open_meteo_ensemble_entries
|
||||||
|
)
|
||||||
|
gauge_set(
|
||||||
|
"polyweather_open_meteo_multi_model_cache_entries",
|
||||||
|
open_meteo_multi_model_entries,
|
||||||
|
)
|
||||||
gauge_set("polyweather_metar_cache_entries", metar_entries)
|
gauge_set("polyweather_metar_cache_entries", metar_entries)
|
||||||
gauge_set("polyweather_taf_cache_entries", taf_entries)
|
gauge_set("polyweather_taf_cache_entries", taf_entries)
|
||||||
gauge_set("polyweather_settlement_cache_entries", settlement_entries)
|
gauge_set("polyweather_settlement_cache_entries", settlement_entries)
|
||||||
@@ -475,8 +505,12 @@ def _integration_summary() -> Dict[str, Any]:
|
|||||||
weather_cfg = _config.get("weather", {}) if isinstance(_config, dict) else {}
|
weather_cfg = _config.get("weather", {}) if isinstance(_config, dict) else {}
|
||||||
return {
|
return {
|
||||||
"supabase_configured": bool(SUPABASE_ENTITLEMENT.configured),
|
"supabase_configured": bool(SUPABASE_ENTITLEMENT.configured),
|
||||||
"telegram_bot_configured": bool((_config.get("telegram", {}) or {}).get("bot_token")),
|
"telegram_bot_configured": bool(
|
||||||
"walletconnect_configured": bool(os.getenv("NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID")),
|
(_config.get("telegram", {}) or {}).get("bot_token")
|
||||||
|
),
|
||||||
|
"walletconnect_configured": bool(
|
||||||
|
os.getenv("NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID")
|
||||||
|
),
|
||||||
"weather_sources": {
|
"weather_sources": {
|
||||||
"openweather": bool(weather_cfg.get("openweather_api_key")),
|
"openweather": bool(weather_cfg.get("openweather_api_key")),
|
||||||
"wunderground": bool(weather_cfg.get("wunderground_api_key")),
|
"wunderground": bool(weather_cfg.get("wunderground_api_key")),
|
||||||
@@ -603,7 +637,9 @@ def _city_coverage_summary(conn: sqlite3.Connection) -> Dict[str, Any]:
|
|||||||
"city": city,
|
"city": city,
|
||||||
"name": str(meta.get("name") or city),
|
"name": str(meta.get("name") or city),
|
||||||
"settlement_source": str(meta.get("settlement_source") or "metar"),
|
"settlement_source": str(meta.get("settlement_source") or "metar"),
|
||||||
"settlement_station_code": str(meta.get("settlement_station_code") or meta.get("icao") or ""),
|
"settlement_station_code": str(
|
||||||
|
meta.get("settlement_station_code") or meta.get("icao") or ""
|
||||||
|
),
|
||||||
"truth_rows": int(truth_payload.get("truth_rows") or 0),
|
"truth_rows": int(truth_payload.get("truth_rows") or 0),
|
||||||
"feature_rows": int(feature_payload.get("feature_rows") or 0),
|
"feature_rows": int(feature_payload.get("feature_rows") or 0),
|
||||||
"truth_min_date": truth_payload.get("truth_min_date"),
|
"truth_min_date": truth_payload.get("truth_min_date"),
|
||||||
@@ -614,9 +650,7 @@ def _city_coverage_summary(conn: sqlite3.Connection) -> Dict[str, Any]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
highlighted = [
|
highlighted = [
|
||||||
entry
|
entry for entry in entries if entry["city"] in {"taipei", "shenzhen"}
|
||||||
for entry in entries
|
|
||||||
if entry["city"] in {"taipei", "shenzhen"}
|
|
||||||
]
|
]
|
||||||
gaps = sorted(
|
gaps = sorted(
|
||||||
entries,
|
entries,
|
||||||
@@ -687,7 +721,9 @@ def _training_data_summary() -> Dict[str, Any]:
|
|||||||
if truth_records.get("ok"):
|
if truth_records.get("ok"):
|
||||||
truth_records["source_counts"] = _truth_source_counts(conn)
|
truth_records["source_counts"] = _truth_source_counts(conn)
|
||||||
truth_revisions = _truth_revisions_summary(conn)
|
truth_revisions = _truth_revisions_summary(conn)
|
||||||
training_features = _table_date_summary(conn, "training_feature_records_store")
|
training_features = _table_date_summary(
|
||||||
|
conn, "training_feature_records_store"
|
||||||
|
)
|
||||||
city_coverage = _city_coverage_summary(conn)
|
city_coverage = _city_coverage_summary(conn)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return {
|
return {
|
||||||
|
|||||||
+23
-131
@@ -4,8 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from src.analysis.settlement_rounding import apply_city_settlement
|
from web.core import _is_excluded_model_name
|
||||||
from web.core import _is_excluded_model_name, _market_layer, _sf
|
|
||||||
|
|
||||||
TURKISH_MGM_CITIES = {"ankara", "istanbul"}
|
TURKISH_MGM_CITIES = {"ankara", "istanbul"}
|
||||||
|
|
||||||
@@ -22,7 +21,9 @@ def build_city_summary_payload(data: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
"temp": data.get("current", {}).get("temp"),
|
"temp": data.get("current", {}).get("temp"),
|
||||||
"obs_time": data.get("current", {}).get("obs_time"),
|
"obs_time": data.get("current", {}).get("obs_time"),
|
||||||
"settlement_source": data.get("current", {}).get("settlement_source"),
|
"settlement_source": data.get("current", {}).get("settlement_source"),
|
||||||
"settlement_source_label": data.get("current", {}).get("settlement_source_label"),
|
"settlement_source_label": data.get("current", {}).get(
|
||||||
|
"settlement_source_label"
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"deb": {"prediction": data.get("deb", {}).get("prediction")},
|
"deb": {"prediction": data.get("deb", {}).get("prediction")},
|
||||||
"deviation_monitor": data.get("deviation_monitor") or {},
|
"deviation_monitor": data.get("deviation_monitor") or {},
|
||||||
@@ -41,135 +42,13 @@ def build_city_market_scan_payload(
|
|||||||
lite: bool = False,
|
lite: bool = False,
|
||||||
scan_filters: Optional[Dict[str, Any]] = None,
|
scan_filters: Optional[Dict[str, Any]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
city = str(data.get("name") or "").strip().lower()
|
|
||||||
local_date = str(data.get("local_date") or "").strip()
|
local_date = str(data.get("local_date") or "").strip()
|
||||||
requested_date = str(target_date or "").strip()
|
requested_date = str(target_date or "").strip()
|
||||||
selected_date = requested_date or local_date
|
selected_date = requested_date or local_date
|
||||||
|
|
||||||
multi_model_daily = data.get("multi_model_daily") or {}
|
|
||||||
selected_daily = (
|
|
||||||
multi_model_daily.get(selected_date)
|
|
||||||
if isinstance(multi_model_daily, dict)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
if not isinstance(selected_daily, dict):
|
|
||||||
selected_daily = {}
|
|
||||||
selected_date = local_date
|
|
||||||
|
|
||||||
distribution = selected_daily.get("probabilities")
|
|
||||||
if not isinstance(distribution, list) or not distribution:
|
|
||||||
distribution = data.get("probabilities", {}).get("distribution", []) or []
|
|
||||||
distribution_all = selected_daily.get("probabilities_all")
|
|
||||||
if not isinstance(distribution_all, list) or not distribution_all:
|
|
||||||
distribution_all = data.get("probabilities", {}).get("distribution_all", []) or []
|
|
||||||
if not distribution_all:
|
|
||||||
distribution_all = distribution
|
|
||||||
|
|
||||||
model_map = selected_daily.get("models") or data.get("multi_model") or {}
|
|
||||||
if not isinstance(model_map, dict):
|
|
||||||
model_map = {}
|
|
||||||
|
|
||||||
anchor_temp = None
|
|
||||||
anchor_model = None
|
|
||||||
for model_name, raw_value in model_map.items():
|
|
||||||
value = _sf(raw_value)
|
|
||||||
if value is None:
|
|
||||||
continue
|
|
||||||
if anchor_temp is None or value > anchor_temp:
|
|
||||||
anchor_temp = value
|
|
||||||
anchor_model = str(model_name or "").strip() or None
|
|
||||||
|
|
||||||
anchor_temp_c = anchor_temp
|
|
||||||
temp_symbol = str(data.get("temp_symbol") or "")
|
|
||||||
if anchor_temp_c is not None and "F" in temp_symbol.upper():
|
|
||||||
anchor_temp_c = (anchor_temp_c - 32.0) * 5.0 / 9.0
|
|
||||||
anchor_settlement = apply_city_settlement(city, anchor_temp_c) if anchor_temp_c is not None else None
|
|
||||||
|
|
||||||
primary_bucket = None
|
|
||||||
if isinstance(distribution, list) and distribution:
|
|
||||||
ranked_buckets = []
|
|
||||||
temp_symbol_upper = str(temp_symbol or "").upper()
|
|
||||||
max_primary_bucket_delta = 16.0 if "F" in temp_symbol_upper else 8.0
|
|
||||||
for idx, row in enumerate(distribution_all):
|
|
||||||
if not isinstance(row, dict):
|
|
||||||
continue
|
|
||||||
bucket_value = _sf(
|
|
||||||
row.get("temp")
|
|
||||||
if row.get("temp") is not None
|
|
||||||
else row.get("value")
|
|
||||||
if row.get("value") is not None
|
|
||||||
else row.get("lower")
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
anchor_temp is not None
|
|
||||||
and bucket_value is not None
|
|
||||||
and abs(float(bucket_value) - float(anchor_temp)) > max_primary_bucket_delta
|
|
||||||
):
|
|
||||||
continue
|
|
||||||
bucket_prob = _sf(row.get("probability"))
|
|
||||||
prob_rank = bucket_prob if bucket_prob is not None else -1.0
|
|
||||||
ranked_buckets.append((-prob_rank, idx, row))
|
|
||||||
if ranked_buckets:
|
|
||||||
ranked_buckets.sort(key=lambda x: (x[0], x[1]))
|
|
||||||
primary_bucket = ranked_buckets[0][2]
|
|
||||||
elif anchor_temp is None:
|
|
||||||
primary_bucket = distribution[0]
|
|
||||||
|
|
||||||
model_probability = None
|
|
||||||
if isinstance(primary_bucket, dict) and primary_bucket.get("probability") is not None:
|
|
||||||
try:
|
|
||||||
raw_probability = float(primary_bucket.get("probability"))
|
|
||||||
model_probability = raw_probability / 100.0 if raw_probability > 1.0 else raw_probability
|
|
||||||
except Exception:
|
|
||||||
model_probability = None
|
|
||||||
|
|
||||||
fallback_sparkline = [
|
|
||||||
p.get("probability", 0)
|
|
||||||
for p in distribution_all[:8]
|
|
||||||
if isinstance(p, dict)
|
|
||||||
]
|
|
||||||
current = data.get("current") or {}
|
|
||||||
selected_deb = selected_daily.get("deb") if isinstance(selected_daily.get("deb"), dict) else {}
|
|
||||||
current_deb = data.get("deb") if isinstance(data.get("deb"), dict) else {}
|
|
||||||
scan_context = {
|
|
||||||
"local_date": data.get("local_date"),
|
|
||||||
"local_time": data.get("local_time"),
|
|
||||||
"peak": data.get("peak") or {},
|
|
||||||
"current_max_so_far": current.get("max_so_far"),
|
|
||||||
"current_temp": current.get("temp"),
|
|
||||||
"trend": data.get("trend") or {},
|
|
||||||
"network_lead_signal": data.get("network_lead_signal") or {},
|
|
||||||
"models": model_map,
|
|
||||||
"deb_prediction": selected_deb.get("prediction") or current_deb.get("prediction"),
|
|
||||||
}
|
|
||||||
market_scan = _market_layer.build_market_scan(
|
|
||||||
city=data.get("name"),
|
|
||||||
target_date=selected_date or data.get("local_date"),
|
|
||||||
temperature_bucket=primary_bucket if isinstance(primary_bucket, dict) else None,
|
|
||||||
model_probability=model_probability,
|
|
||||||
probability_distribution=distribution_all,
|
|
||||||
temp_symbol=temp_symbol,
|
|
||||||
fallback_sparkline=fallback_sparkline,
|
|
||||||
forced_market_slug=market_slug,
|
|
||||||
include_related_buckets=not lite,
|
|
||||||
scan_filters=scan_filters,
|
|
||||||
scan_context=scan_context,
|
|
||||||
)
|
|
||||||
if isinstance(market_scan, dict):
|
|
||||||
market_scan["anchor_model"] = anchor_model
|
|
||||||
market_scan["anchor_high"] = anchor_temp
|
|
||||||
market_scan["anchor_settlement"] = anchor_settlement
|
|
||||||
market_scan["open_meteo_settlement"] = anchor_settlement
|
|
||||||
probabilities = data.get("probabilities") or {}
|
|
||||||
market_scan["probability_engine"] = str(
|
|
||||||
probabilities.get("engine") or "legacy"
|
|
||||||
).strip() or "legacy"
|
|
||||||
market_scan["probability_calibration_mode"] = str(
|
|
||||||
probabilities.get("calibration_mode") or "legacy"
|
|
||||||
).strip() or "legacy"
|
|
||||||
return {
|
return {
|
||||||
"market_scan": market_scan,
|
"market_scan": {"available": False},
|
||||||
"selected_date": selected_date or data.get("local_date"),
|
"selected_date": selected_date,
|
||||||
"fetched_at": data.get("updated_at"),
|
"fetched_at": data.get("updated_at"),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +79,9 @@ def build_city_detail_payload(
|
|||||||
"temp_symbol": data.get("temp_symbol"),
|
"temp_symbol": data.get("temp_symbol"),
|
||||||
"current_temp": data.get("current", {}).get("temp"),
|
"current_temp": data.get("current", {}).get("temp"),
|
||||||
"settlement_source": data.get("current", {}).get("settlement_source"),
|
"settlement_source": data.get("current", {}).get("settlement_source"),
|
||||||
"settlement_source_label": data.get("current", {}).get("settlement_source_label"),
|
"settlement_source_label": data.get("current", {}).get(
|
||||||
|
"settlement_source_label"
|
||||||
|
),
|
||||||
"settlement_station": data.get("settlement_station") or {},
|
"settlement_station": data.get("settlement_station") or {},
|
||||||
"deb_prediction": data.get("deb", {}).get("prediction"),
|
"deb_prediction": data.get("deb", {}).get("prediction"),
|
||||||
"risk_level": data.get("risk", {}).get("level"),
|
"risk_level": data.get("risk", {}).get("level"),
|
||||||
@@ -221,7 +102,12 @@ def build_city_detail_payload(
|
|||||||
"weather_gov": {},
|
"weather_gov": {},
|
||||||
"mgm": data.get("mgm") or {},
|
"mgm": data.get("mgm") or {},
|
||||||
"mgm_nearby": data.get("mgm_nearby") or [],
|
"mgm_nearby": data.get("mgm_nearby") or [],
|
||||||
"nearby_source": data.get("nearby_source") or ("mgm" if str(data.get("name") or "").lower() in TURKISH_MGM_CITIES else "metar_cluster"),
|
"nearby_source": data.get("nearby_source")
|
||||||
|
or (
|
||||||
|
"mgm"
|
||||||
|
if str(data.get("name") or "").lower() in TURKISH_MGM_CITIES
|
||||||
|
else "metar_cluster"
|
||||||
|
),
|
||||||
"airport_primary": data.get("airport_primary") or {},
|
"airport_primary": data.get("airport_primary") or {},
|
||||||
"airport_primary_today_obs": data.get("airport_primary_today_obs") or [],
|
"airport_primary_today_obs": data.get("airport_primary_today_obs") or [],
|
||||||
"official_nearby": data.get("official_nearby") or [],
|
"official_nearby": data.get("official_nearby") or [],
|
||||||
@@ -248,7 +134,8 @@ def build_city_detail_payload(
|
|||||||
"deb": data.get("deb") or {},
|
"deb": data.get("deb") or {},
|
||||||
"multi_model_daily": data.get("multi_model_daily") or {},
|
"multi_model_daily": data.get("multi_model_daily") or {},
|
||||||
"probabilities": data.get("probabilities") or {"mu": None, "distribution": []},
|
"probabilities": data.get("probabilities") or {"mu": None, "distribution": []},
|
||||||
"dynamic_commentary": data.get("dynamic_commentary") or {"summary": "", "notes": []},
|
"dynamic_commentary": data.get("dynamic_commentary")
|
||||||
|
or {"summary": "", "notes": []},
|
||||||
"intraday_meteorology": data.get("intraday_meteorology")
|
"intraday_meteorology": data.get("intraday_meteorology")
|
||||||
or _build_intraday_meteorology(data),
|
or _build_intraday_meteorology(data),
|
||||||
"vertical_profile_signal": data.get("vertical_profile_signal") or {},
|
"vertical_profile_signal": data.get("vertical_profile_signal") or {},
|
||||||
@@ -266,7 +153,12 @@ def build_city_detail_payload(
|
|||||||
"airport_vs_network_delta": data.get("airport_vs_network_delta"),
|
"airport_vs_network_delta": data.get("airport_vs_network_delta"),
|
||||||
"airport_current": data.get("airport_current") or {},
|
"airport_current": data.get("airport_current") or {},
|
||||||
"amos": data.get("amos") or {},
|
"amos": data.get("amos") or {},
|
||||||
"nearby_source": data.get("nearby_source") or ("mgm" if str(data.get("name") or "").lower() in TURKISH_MGM_CITIES else "metar_cluster"),
|
"nearby_source": data.get("nearby_source")
|
||||||
|
or (
|
||||||
|
"mgm"
|
||||||
|
if str(data.get("name") or "").lower() in TURKISH_MGM_CITIES
|
||||||
|
else "metar_cluster"
|
||||||
|
),
|
||||||
"ai_analysis": data.get("ai_analysis") or "",
|
"ai_analysis": data.get("ai_analysis") or "",
|
||||||
"errors": {},
|
"errors": {},
|
||||||
}
|
}
|
||||||
|
|||||||
+313
-140
@@ -39,7 +39,9 @@ def list_ops_memberships(request: Request, limit: int = 200) -> Dict[str, Any]:
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
subscriptions = legacy_routes.SUPABASE_ENTITLEMENT.list_active_subscriptions(limit=limit)
|
subscriptions = legacy_routes.SUPABASE_ENTITLEMENT.list_active_subscriptions(
|
||||||
|
limit=limit
|
||||||
|
)
|
||||||
subscription_user_ids = [str(item.get("user_id") or "") for item in subscriptions]
|
subscription_user_ids = [str(item.get("user_id") or "") for item in subscriptions]
|
||||||
user_map = db.get_users_by_supabase_user_ids(subscription_user_ids)
|
user_map = db.get_users_by_supabase_user_ids(subscription_user_ids)
|
||||||
unresolved_user_ids = [
|
unresolved_user_ids = [
|
||||||
@@ -47,19 +49,24 @@ def list_ops_memberships(request: Request, limit: int = 200) -> Dict[str, Any]:
|
|||||||
for user_id in subscription_user_ids
|
for user_id in subscription_user_ids
|
||||||
if str(user_id or "").strip().lower()
|
if str(user_id or "").strip().lower()
|
||||||
and not str(
|
and not str(
|
||||||
(user_map.get(str(user_id).strip().lower(), {}) or {}).get("supabase_email") or ""
|
(user_map.get(str(user_id).strip().lower(), {}) or {}).get("supabase_email")
|
||||||
|
or ""
|
||||||
).strip()
|
).strip()
|
||||||
]
|
]
|
||||||
auth_user_map = legacy_routes.SUPABASE_ENTITLEMENT.get_auth_users(unresolved_user_ids)
|
auth_user_map = legacy_routes.SUPABASE_ENTITLEMENT.get_auth_users(
|
||||||
|
unresolved_user_ids
|
||||||
|
)
|
||||||
deduped: dict[str, dict] = {}
|
deduped: dict[str, dict] = {}
|
||||||
for item in subscriptions:
|
for item in subscriptions:
|
||||||
user_id = str(item.get("user_id") or "").strip().lower()
|
user_id = str(item.get("user_id") or "").strip().lower()
|
||||||
local_user = user_map.get(user_id, {})
|
local_user = user_map.get(user_id, {})
|
||||||
auth_user = auth_user_map.get(user_id, {})
|
auth_user = auth_user_map.get(user_id, {})
|
||||||
subscription_window = legacy_routes.SUPABASE_ENTITLEMENT.get_subscription_window(
|
subscription_window = (
|
||||||
user_id,
|
legacy_routes.SUPABASE_ENTITLEMENT.get_subscription_window(
|
||||||
respect_requirement=False,
|
user_id,
|
||||||
bypass_cache=True,
|
respect_requirement=False,
|
||||||
|
bypass_cache=True,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
current_expires_at = item.get("expires_at")
|
current_expires_at = item.get("expires_at")
|
||||||
total_expires_at = (
|
total_expires_at = (
|
||||||
@@ -78,13 +85,18 @@ def list_ops_memberships(request: Request, limit: int = 200) -> Dict[str, Any]:
|
|||||||
else 0
|
else 0
|
||||||
)
|
)
|
||||||
source = str(item.get("source") or "")
|
source = str(item.get("source") or "")
|
||||||
is_trial = source == "signup_trial" or str(item.get("plan_code") or "").startswith("signup_trial")
|
is_trial = source == "signup_trial" or str(
|
||||||
|
item.get("plan_code") or ""
|
||||||
|
).startswith("signup_trial")
|
||||||
row = {
|
row = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"email": str(auth_user.get("email") or local_user.get("supabase_email") or ""),
|
"email": str(
|
||||||
|
auth_user.get("email") or local_user.get("supabase_email") or ""
|
||||||
|
),
|
||||||
"telegram_id": local_user.get("telegram_id"),
|
"telegram_id": local_user.get("telegram_id"),
|
||||||
"username": local_user.get("username"),
|
"username": local_user.get("username"),
|
||||||
"registered_at": local_user.get("created_at") or auth_user.get("created_at"),
|
"registered_at": local_user.get("created_at")
|
||||||
|
or auth_user.get("created_at"),
|
||||||
"plan_code": item.get("plan_code"),
|
"plan_code": item.get("plan_code"),
|
||||||
"source": source,
|
"source": source,
|
||||||
"is_trial": is_trial,
|
"is_trial": is_trial,
|
||||||
@@ -113,7 +125,9 @@ def get_ops_memberships_growth(request: Request, days: int = 90) -> dict[str, An
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
safe_days = max(7, min(365, int(days or 90)))
|
safe_days = max(7, min(365, int(days or 90)))
|
||||||
subscriptions = legacy_routes.SUPABASE_ENTITLEMENT.list_active_subscriptions(limit=5000)
|
subscriptions = legacy_routes.SUPABASE_ENTITLEMENT.list_active_subscriptions(
|
||||||
|
limit=5000
|
||||||
|
)
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
cutoff = now - timedelta(days=safe_days)
|
cutoff = now - timedelta(days=safe_days)
|
||||||
|
|
||||||
@@ -150,7 +164,15 @@ def get_ops_memberships_growth(request: Request, days: int = 90) -> dict[str, An
|
|||||||
pc = paid_by_day.get(key, 0)
|
pc = paid_by_day.get(key, 0)
|
||||||
total = tc + pc
|
total = tc + pc
|
||||||
running += total
|
running += total
|
||||||
daily.append({"date": key, "trial": tc, "paid": pc, "total": total, "cumulative": running})
|
daily.append(
|
||||||
|
{
|
||||||
|
"date": key,
|
||||||
|
"trial": tc,
|
||||||
|
"paid": pc,
|
||||||
|
"total": total,
|
||||||
|
"cumulative": running,
|
||||||
|
}
|
||||||
|
)
|
||||||
cursor += timedelta(days=1)
|
cursor += timedelta(days=1)
|
||||||
|
|
||||||
return {"days": safe_days, "daily": daily}
|
return {"days": safe_days, "daily": daily}
|
||||||
@@ -186,7 +208,9 @@ def list_ops_payment_incidents(
|
|||||||
def resolve_ops_payment_incident(request: Request, event_id: int) -> Dict[str, Any]:
|
def resolve_ops_payment_incident(request: Request, event_id: int) -> Dict[str, Any]:
|
||||||
admin = _require_ops(request) or {}
|
admin = _require_ops(request) or {}
|
||||||
db = DBManager()
|
db = DBManager()
|
||||||
resolved = db.mark_payment_audit_event_resolved(event_id, str(admin.get("email") or ""))
|
resolved = db.mark_payment_audit_event_resolved(
|
||||||
|
event_id, str(admin.get("email") or "")
|
||||||
|
)
|
||||||
if not resolved:
|
if not resolved:
|
||||||
raise HTTPException(status_code=404, detail="payment_incident_not_found")
|
raise HTTPException(status_code=404, detail="payment_incident_not_found")
|
||||||
return {"ok": True, "incident": resolved}
|
return {"ok": True, "incident": resolved}
|
||||||
@@ -251,7 +275,9 @@ def transfer_ops_points(
|
|||||||
to_email = str(to_email or "").strip()
|
to_email = str(to_email or "").strip()
|
||||||
amount = int(amount or 0)
|
amount = int(amount or 0)
|
||||||
if not from_email or not to_email:
|
if not from_email or not to_email:
|
||||||
raise HTTPException(status_code=400, detail="from_email and to_email are required")
|
raise HTTPException(
|
||||||
|
status_code=400, detail="from_email and to_email are required"
|
||||||
|
)
|
||||||
if amount <= 0:
|
if amount <= 0:
|
||||||
raise HTTPException(status_code=400, detail="amount must be positive")
|
raise HTTPException(status_code=400, detail="amount must be positive")
|
||||||
db = DBManager()
|
db = DBManager()
|
||||||
@@ -300,7 +326,8 @@ def get_ops_truth_history(
|
|||||||
{
|
{
|
||||||
"city": row_city,
|
"city": row_city,
|
||||||
"display_name": str(
|
"display_name": str(
|
||||||
(legacy_routes.CITY_REGISTRY.get(row_city) or {}).get("name") or row_city
|
(legacy_routes.CITY_REGISTRY.get(row_city) or {}).get("name")
|
||||||
|
or row_city
|
||||||
),
|
),
|
||||||
"target_date": str(target_date),
|
"target_date": str(target_date),
|
||||||
"actual_high": payload.get("actual_high"),
|
"actual_high": payload.get("actual_high"),
|
||||||
@@ -314,7 +341,9 @@ def get_ops_truth_history(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
rows.sort(key=lambda item: (str(item["target_date"]), str(item["city"])), reverse=True)
|
rows.sort(
|
||||||
|
key=lambda item: (str(item["target_date"]), str(item["city"])), reverse=True
|
||||||
|
)
|
||||||
filtered_count = len(rows)
|
filtered_count = len(rows)
|
||||||
rows = rows[:max_limit]
|
rows = rows[:max_limit]
|
||||||
available_cities = [
|
available_cities = [
|
||||||
@@ -358,27 +387,34 @@ _EDITABLE_CONFIG_KEYS: dict[str, str] = {
|
|||||||
def get_ops_config(request: Request) -> dict[str, Any]:
|
def get_ops_config(request: Request) -> dict[str, Any]:
|
||||||
_require_ops(request)
|
_require_ops(request)
|
||||||
import os
|
import os
|
||||||
|
|
||||||
configs: list[dict[str, str]] = []
|
configs: list[dict[str, str]] = []
|
||||||
for key, desc in _EDITABLE_CONFIG_KEYS.items():
|
for key, desc in _EDITABLE_CONFIG_KEYS.items():
|
||||||
configs.append({
|
configs.append(
|
||||||
"key": key,
|
{
|
||||||
"value": os.getenv(key) or "",
|
"key": key,
|
||||||
"description": desc,
|
"value": os.getenv(key) or "",
|
||||||
})
|
"description": desc,
|
||||||
|
}
|
||||||
|
)
|
||||||
return {"configs": configs}
|
return {"configs": configs}
|
||||||
|
|
||||||
|
|
||||||
def update_ops_config(request: Request, key: str, value: str) -> dict[str, Any]:
|
def update_ops_config(request: Request, key: str, value: str) -> dict[str, Any]:
|
||||||
_require_ops(request)
|
_require_ops(request)
|
||||||
import os
|
import os
|
||||||
|
|
||||||
if key not in _EDITABLE_CONFIG_KEYS:
|
if key not in _EDITABLE_CONFIG_KEYS:
|
||||||
raise HTTPException(status_code=400, detail=f"config key '{key}' is not editable")
|
raise HTTPException(
|
||||||
|
status_code=400, detail=f"config key '{key}' is not editable"
|
||||||
|
)
|
||||||
os.environ[key] = str(value)
|
os.environ[key] = str(value)
|
||||||
return {"key": key, "value": value, "ok": True}
|
return {"key": key, "value": value, "ok": True}
|
||||||
|
|
||||||
|
|
||||||
# ── Subscriptions ───────────────────────────────────────────────────
|
# ── Subscriptions ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def grant_ops_subscription(
|
def grant_ops_subscription(
|
||||||
request: Request,
|
request: Request,
|
||||||
email: str,
|
email: str,
|
||||||
@@ -398,7 +434,9 @@ def grant_ops_subscription(
|
|||||||
|
|
||||||
allowed_plans = {"pro_monthly"}
|
allowed_plans = {"pro_monthly"}
|
||||||
if plan_code not in allowed_plans:
|
if plan_code not in allowed_plans:
|
||||||
raise HTTPException(status_code=400, detail=f"invalid plan_code, allowed: {allowed_plans}")
|
raise HTTPException(
|
||||||
|
status_code=400, detail=f"invalid plan_code, allowed: {allowed_plans}"
|
||||||
|
)
|
||||||
|
|
||||||
safe_days = max(1, min(365, int(days or 30)))
|
safe_days = max(1, min(365, int(days or 30)))
|
||||||
safe_deduct = max(0, int(deduct_points or 0))
|
safe_deduct = max(0, int(deduct_points or 0))
|
||||||
@@ -423,7 +461,9 @@ def grant_ops_subscription(
|
|||||||
users = user_resp.json().get("users", []) if user_resp.ok else []
|
users = user_resp.json().get("users", []) if user_resp.ok else []
|
||||||
user_id = users[0].get("id") if users else None
|
user_id = users[0].get("id") if users else None
|
||||||
if not user_id:
|
if not user_id:
|
||||||
raise HTTPException(status_code=404, detail=f"user not found: {normalized_email}")
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"user not found: {normalized_email}"
|
||||||
|
)
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
starts_at = now.isoformat() + "Z"
|
starts_at = now.isoformat() + "Z"
|
||||||
@@ -446,17 +486,24 @@ def grant_ops_subscription(
|
|||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
if not resp.ok:
|
if not resp.ok:
|
||||||
raise HTTPException(status_code=500, detail=f"Supabase insert failed: {resp.text[:200]}")
|
raise HTTPException(
|
||||||
|
status_code=500, detail=f"Supabase insert failed: {resp.text[:200]}"
|
||||||
|
)
|
||||||
|
|
||||||
result: dict[str, Any] = {
|
result: dict[str, Any] = {
|
||||||
"ok": True, "user_id": user_id, "plan_code": plan_code,
|
"ok": True,
|
||||||
"days": safe_days, "expires_at": expires_at,
|
"user_id": user_id,
|
||||||
|
"plan_code": plan_code,
|
||||||
|
"days": safe_days,
|
||||||
|
"expires_at": expires_at,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Optionally deduct points from the user (manual Pro grant with points payment)
|
# Optionally deduct points from the user (manual Pro grant with points payment)
|
||||||
if safe_deduct > 0:
|
if safe_deduct > 0:
|
||||||
db = DBManager()
|
db = DBManager()
|
||||||
deduct_result = db.deduct_points_by_supabase_email(normalized_email, safe_deduct)
|
deduct_result = db.deduct_points_by_supabase_email(
|
||||||
|
normalized_email, safe_deduct
|
||||||
|
)
|
||||||
result["points_deducted"] = safe_deduct
|
result["points_deducted"] = safe_deduct
|
||||||
result["points_result"] = deduct_result
|
result["points_result"] = deduct_result
|
||||||
|
|
||||||
@@ -504,7 +551,9 @@ def extend_ops_subscription(
|
|||||||
)
|
)
|
||||||
subs = subs_resp.json() if subs_resp.ok else []
|
subs = subs_resp.json() if subs_resp.ok else []
|
||||||
if not subs:
|
if not subs:
|
||||||
raise HTTPException(status_code=404, detail=f"no subscription found for {normalized_email}")
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"no subscription found for {normalized_email}"
|
||||||
|
)
|
||||||
|
|
||||||
sub = subs[0]
|
sub = subs[0]
|
||||||
current_expiry = sub.get("expires_at", "")
|
current_expiry = sub.get("expires_at", "")
|
||||||
@@ -521,8 +570,15 @@ def extend_ops_subscription(
|
|||||||
timeout=10,
|
timeout=10,
|
||||||
)
|
)
|
||||||
if patch_resp.ok:
|
if patch_resp.ok:
|
||||||
return {"ok": True, "email": normalized_email, "additional_days": safe_days, "new_expires_at": new_expiry}
|
return {
|
||||||
raise HTTPException(status_code=500, detail=f"Supabase update failed: {patch_resp.text[:200]}")
|
"ok": True,
|
||||||
|
"email": normalized_email,
|
||||||
|
"additional_days": safe_days,
|
||||||
|
"new_expires_at": new_expiry,
|
||||||
|
}
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail=f"Supabase update failed: {patch_resp.text[:200]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_ops_user_subscriptions(
|
def get_ops_user_subscriptions(
|
||||||
@@ -559,7 +615,9 @@ def get_ops_user_subscriptions(
|
|||||||
users = user_resp.json().get("users", []) if user_resp.ok else []
|
users = user_resp.json().get("users", []) if user_resp.ok else []
|
||||||
user_id = users[0].get("id") if users else None
|
user_id = users[0].get("id") if users else None
|
||||||
if not user_id:
|
if not user_id:
|
||||||
raise HTTPException(status_code=404, detail=f"user not found: {normalized_email}")
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"user not found: {normalized_email}"
|
||||||
|
)
|
||||||
|
|
||||||
# Fetch all subscription rows for this user (no status filter)
|
# Fetch all subscription rows for this user (no status filter)
|
||||||
subs_resp = _requests.get(
|
subs_resp = _requests.get(
|
||||||
@@ -587,6 +645,7 @@ def get_ops_user_subscriptions(
|
|||||||
|
|
||||||
# ── Logs ────────────────────────────────────────────────────────────
|
# ── Logs ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def get_ops_logs(
|
def get_ops_logs(
|
||||||
request: Request,
|
request: Request,
|
||||||
level: str = "",
|
level: str = "",
|
||||||
@@ -595,6 +654,7 @@ def get_ops_logs(
|
|||||||
_require_ops(request)
|
_require_ops(request)
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
safe_lines = max(10, min(1000, int(lines or 100)))
|
safe_lines = max(10, min(1000, int(lines or 100)))
|
||||||
log_text = ""
|
log_text = ""
|
||||||
try:
|
try:
|
||||||
@@ -649,8 +709,19 @@ def get_ops_health_check(request: Request) -> dict[str, Any]:
|
|||||||
supabase_key = str(os.getenv("SUPABASE_SERVICE_ROLE_KEY") or "").strip()
|
supabase_key = str(os.getenv("SUPABASE_SERVICE_ROLE_KEY") or "").strip()
|
||||||
if supabase_url and supabase_key:
|
if supabase_url and supabase_key:
|
||||||
try:
|
try:
|
||||||
r = _r.get(f"{supabase_url}/rest/v1/", headers={"apikey": supabase_key, "Authorization": f"Bearer {supabase_key}"}, timeout=timeout)
|
r = _r.get(
|
||||||
results["supabase"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round(r.elapsed.total_seconds() * 1000)}
|
f"{supabase_url}/rest/v1/",
|
||||||
|
headers={
|
||||||
|
"apikey": supabase_key,
|
||||||
|
"Authorization": f"Bearer {supabase_key}",
|
||||||
|
},
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
results["supabase"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round(r.elapsed.total_seconds() * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["supabase"] = {"ok": False, "error": str(e)[:100]}
|
results["supabase"] = {"ok": False, "error": str(e)[:100]}
|
||||||
else:
|
else:
|
||||||
@@ -659,16 +730,30 @@ def get_ops_health_check(request: Request) -> dict[str, Any]:
|
|||||||
# Open-Meteo
|
# Open-Meteo
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&daily=temperature_2m_max&timezone=auto&forecast_days=1", timeout=timeout)
|
r = _r.get(
|
||||||
results["open_meteo"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
"https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&daily=temperature_2m_max&timezone=auto&forecast_days=1",
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
results["open_meteo"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["open_meteo"] = {"ok": False, "error": str(e)[:100]}
|
results["open_meteo"] = {"ok": False, "error": str(e)[:100]}
|
||||||
|
|
||||||
# METAR (aviationweather)
|
# METAR (aviationweather)
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://aviationweather.gov/api/data/metar?ids=KJFK&format=json", timeout=timeout)
|
r = _r.get(
|
||||||
results["metar"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
"https://aviationweather.gov/api/data/metar?ids=KJFK&format=json",
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
results["metar"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["metar"] = {"ok": False, "error": str(e)[:100]}
|
results["metar"] = {"ok": False, "error": str(e)[:100]}
|
||||||
|
|
||||||
@@ -677,8 +762,16 @@ def get_ops_health_check(request: Request) -> dict[str, Any]:
|
|||||||
if knmi_key:
|
if knmi_key:
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://api.dataplatform.knmi.nl/open-data/v1/datasets/10-minute-in-situ-meteorological-observations/versions/1.0/files?maxKeys=1", headers={"Authorization": knmi_key}, timeout=timeout)
|
r = _r.get(
|
||||||
results["knmi"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
"https://api.dataplatform.knmi.nl/open-data/v1/datasets/10-minute-in-situ-meteorological-observations/versions/1.0/files?maxKeys=1",
|
||||||
|
headers={"Authorization": knmi_key},
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
results["knmi"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["knmi"] = {"ok": False, "error": str(e)[:100]}
|
results["knmi"] = {"ok": False, "error": str(e)[:100]}
|
||||||
else:
|
else:
|
||||||
@@ -687,8 +780,15 @@ def get_ops_health_check(request: Request) -> dict[str, Any]:
|
|||||||
# MADIS (NOAA)
|
# MADIS (NOAA)
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://madis-data.ncep.noaa.gov/madisPublic1/data/LDAD/hfmetar/netCDF/", timeout=timeout)
|
r = _r.get(
|
||||||
results["madis"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
"https://madis-data.ncep.noaa.gov/madisPublic1/data/LDAD/hfmetar/netCDF/",
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
results["madis"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["madis"] = {"ok": False, "error": str(e)[:100]}
|
results["madis"] = {"ok": False, "error": str(e)[:100]}
|
||||||
|
|
||||||
@@ -697,8 +797,14 @@ def get_ops_health_check(request: Request) -> dict[str, Any]:
|
|||||||
if bot_token:
|
if bot_token:
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get(f"https://api.telegram.org/bot{bot_token}/getMe", timeout=timeout)
|
r = _r.get(
|
||||||
results["telegram"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
f"https://api.telegram.org/bot{bot_token}/getMe", timeout=timeout
|
||||||
|
)
|
||||||
|
results["telegram"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["telegram"] = {"ok": False, "error": str(e)[:100]}
|
results["telegram"] = {"ok": False, "error": str(e)[:100]}
|
||||||
else:
|
else:
|
||||||
@@ -708,86 +814,125 @@ def get_ops_health_check(request: Request) -> dict[str, Any]:
|
|||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://www.jma.go.jp/bosai/forecast/", timeout=timeout)
|
r = _r.get("https://www.jma.go.jp/bosai/forecast/", timeout=timeout)
|
||||||
results["jma"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
results["jma"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["jma"] = {"ok": False, "error": str(e)[:100]}
|
results["jma"] = {"ok": False, "error": str(e)[:100]}
|
||||||
|
|
||||||
# MGM (Turkish State Meteorological Service)
|
# MGM (Turkish State Meteorological Service)
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://servis.mgm.gov.tr/web/sondurumlar?istno=17130&_=1", timeout=timeout, headers={"Origin": "https://www.mgm.gov.tr"})
|
r = _r.get(
|
||||||
results["mgm"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
"https://servis.mgm.gov.tr/web/sondurumlar?istno=17130&_=1",
|
||||||
|
timeout=timeout,
|
||||||
|
headers={"Origin": "https://www.mgm.gov.tr"},
|
||||||
|
)
|
||||||
|
results["mgm"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["mgm"] = {"ok": False, "error": str(e)[:100]}
|
results["mgm"] = {"ok": False, "error": str(e)[:100]}
|
||||||
|
|
||||||
# FMI (Finnish Meteorological Institute)
|
# FMI (Finnish Meteorological Institute)
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=GetCapabilities", timeout=timeout)
|
r = _r.get(
|
||||||
results["fmi"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
"https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=GetCapabilities",
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
results["fmi"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["fmi"] = {"ok": False, "error": str(e)[:100]}
|
results["fmi"] = {"ok": False, "error": str(e)[:100]}
|
||||||
|
|
||||||
# KMA (Korea Meteorological Administration)
|
# KMA (Korea Meteorological Administration)
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://www.weather.go.kr/wgis-nuri/js/info/sfc.geojson", timeout=timeout)
|
r = _r.get(
|
||||||
results["kma"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
"https://www.weather.go.kr/wgis-nuri/js/info/sfc.geojson", timeout=timeout
|
||||||
|
)
|
||||||
|
results["kma"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["kma"] = {"ok": False, "error": str(e)[:100]}
|
results["kma"] = {"ok": False, "error": str(e)[:100]}
|
||||||
|
|
||||||
# HKO (Hong Kong Observatory)
|
# HKO (Hong Kong Observatory)
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://data.weather.gov.hk/weatherAPI/hko_data/regional-weather/latest_1min_temperature.csv", timeout=timeout)
|
r = _r.get(
|
||||||
results["hko"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
"https://data.weather.gov.hk/weatherAPI/hko_data/regional-weather/latest_1min_temperature.csv",
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
results["hko"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["hko"] = {"ok": False, "error": str(e)[:100]}
|
results["hko"] = {"ok": False, "error": str(e)[:100]}
|
||||||
|
|
||||||
# Singapore MSS (data.gov.sg)
|
# Singapore MSS (data.gov.sg)
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://api.data.gov.sg/v1/environment/air-temperature", timeout=timeout)
|
r = _r.get(
|
||||||
results["singapore_mss"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
"https://api.data.gov.sg/v1/environment/air-temperature", timeout=timeout
|
||||||
|
)
|
||||||
|
results["singapore_mss"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["singapore_mss"] = {"ok": False, "error": str(e)[:100]}
|
results["singapore_mss"] = {"ok": False, "error": str(e)[:100]}
|
||||||
|
|
||||||
# CWA (Taiwan Central Weather Administration)
|
# CWA (Taiwan Central Weather Administration)
|
||||||
cwa_key = str(os.getenv("CWA_API_KEY") or os.getenv("CWA_OPEN_DATA_AUTH") or os.getenv("CWA_OPEN_DATA_API_KEY") or "").strip()
|
cwa_key = str(
|
||||||
|
os.getenv("CWA_API_KEY")
|
||||||
|
or os.getenv("CWA_OPEN_DATA_AUTH")
|
||||||
|
or os.getenv("CWA_OPEN_DATA_API_KEY")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
if cwa_key:
|
if cwa_key:
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get(f"https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0003-001?Authorization={cwa_key}&limit=1", timeout=timeout)
|
r = _r.get(
|
||||||
results["cwa"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
f"https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0003-001?Authorization={cwa_key}&limit=1",
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
results["cwa"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["cwa"] = {"ok": False, "error": str(e)[:100]}
|
results["cwa"] = {"ok": False, "error": str(e)[:100]}
|
||||||
else:
|
else:
|
||||||
results["cwa"] = {"ok": False, "error": "not configured"}
|
results["cwa"] = {"ok": False, "error": "not configured"}
|
||||||
|
|
||||||
# Polymarket Gamma API
|
# Polymarket health checks have been removed from the project
|
||||||
try:
|
|
||||||
t0 = _time.perf_counter()
|
|
||||||
r = _r.get("https://gamma-api.polymarket.com/events?limit=1", timeout=timeout)
|
|
||||||
results["polymarket_gamma"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
|
||||||
except Exception as e:
|
|
||||||
results["polymarket_gamma"] = {"ok": False, "error": str(e)[:100]}
|
|
||||||
|
|
||||||
# Polymarket CLOB API — GET / returns 200 on healthy CLOB
|
|
||||||
try:
|
|
||||||
t0 = _time.perf_counter()
|
|
||||||
r = _r.get("https://clob.polymarket.com/", timeout=timeout)
|
|
||||||
results["polymarket_clob"] = {"ok": r.status_code < 500, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
|
||||||
except Exception as e:
|
|
||||||
results["polymarket_clob"] = {"ok": False, "error": str(e)[:100]}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# AMOS (Korea runway sensors)
|
# AMOS (Korea runway sensors)
|
||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://global.amo.go.kr/amosobsnew/AmosRealTimeImage.do", timeout=timeout)
|
r = _r.get(
|
||||||
results["amos"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
"https://global.amo.go.kr/amosobsnew/AmosRealTimeImage.do", timeout=timeout
|
||||||
|
)
|
||||||
|
results["amos"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["amos"] = {"ok": False, "error": str(e)[:100]}
|
results["amos"] = {"ok": False, "error": str(e)[:100]}
|
||||||
|
|
||||||
@@ -802,11 +947,17 @@ def get_ops_health_check(request: Request) -> dict[str, Any]:
|
|||||||
verify=False,
|
verify=False,
|
||||||
headers={
|
headers={
|
||||||
"Accept": "application/json, text/plain, */*",
|
"Accept": "application/json, text/plain, */*",
|
||||||
"Referer": os.getenv("AMSC_AWOS_REFERER", "https://www.amsc.net.cn/"),
|
"Referer": os.getenv(
|
||||||
|
"AMSC_AWOS_REFERER", "https://www.amsc.net.cn/"
|
||||||
|
),
|
||||||
"User-Agent": "PolyWeather/1.0",
|
"User-Agent": "PolyWeather/1.0",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
results["amsc_awos"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
results["amsc_awos"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["amsc_awos"] = {"ok": False, "error": str(e)[:100]}
|
results["amsc_awos"] = {"ok": False, "error": str(e)[:100]}
|
||||||
else:
|
else:
|
||||||
@@ -816,12 +967,20 @@ def get_ops_health_check(request: Request) -> dict[str, Any]:
|
|||||||
try:
|
try:
|
||||||
t0 = _time.perf_counter()
|
t0 = _time.perf_counter()
|
||||||
r = _r.get("https://www.weather.gov/wrh/timeseries?site=KJFK", timeout=timeout)
|
r = _r.get("https://www.weather.gov/wrh/timeseries?site=KJFK", timeout=timeout)
|
||||||
results["noaa_wrh"] = {"ok": r.ok, "status": r.status_code, "latency_ms": round((_time.perf_counter() - t0) * 1000)}
|
results["noaa_wrh"] = {
|
||||||
|
"ok": r.ok,
|
||||||
|
"status": r.status_code,
|
||||||
|
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||||
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
results["noaa_wrh"] = {"ok": False, "error": str(e)[:100]}
|
results["noaa_wrh"] = {"ok": False, "error": str(e)[:100]}
|
||||||
|
|
||||||
all_ok = all(v.get("ok") for v in results.values())
|
all_ok = all(v.get("ok") for v in results.values())
|
||||||
return {"ok": all_ok, "checked_at": __import__("datetime").datetime.utcnow().isoformat() + "Z", "services": results}
|
return {
|
||||||
|
"ok": all_ok,
|
||||||
|
"checked_at": __import__("datetime").datetime.utcnow().isoformat() + "Z",
|
||||||
|
"services": results,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_ops_training_accuracy(request: Request) -> Dict[str, Any]:
|
def get_ops_training_accuracy(request: Request) -> Dict[str, Any]:
|
||||||
@@ -841,7 +1000,7 @@ def get_ops_training_accuracy(request: Request) -> Dict[str, Any]:
|
|||||||
"hit_rate": deb_acc[0],
|
"hit_rate": deb_acc[0],
|
||||||
"mae": deb_acc[1],
|
"mae": deb_acc[1],
|
||||||
"total_days": deb_acc[2],
|
"total_days": deb_acc[2],
|
||||||
"details_str": deb_acc[3]
|
"details_str": deb_acc[3],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate Mu accuracy
|
# Calculate Mu accuracy
|
||||||
@@ -853,24 +1012,21 @@ def get_ops_training_accuracy(request: Request) -> Dict[str, Any]:
|
|||||||
"hit_rate": mu_acc[1],
|
"hit_rate": mu_acc[1],
|
||||||
"brier_score": mu_acc[2],
|
"brier_score": mu_acc[2],
|
||||||
"total_days": mu_acc[3],
|
"total_days": mu_acc[3],
|
||||||
"details_str": mu_acc[4]
|
"details_str": mu_acc[4],
|
||||||
}
|
}
|
||||||
|
|
||||||
if deb_payload or mu_payload:
|
if deb_payload or mu_payload:
|
||||||
accuracy_data.append({
|
accuracy_data.append(
|
||||||
"city_id": city_id,
|
{"city_id": city_id, "name": name, "deb": deb_payload, "mu": mu_payload}
|
||||||
"name": name,
|
)
|
||||||
"deb": deb_payload,
|
|
||||||
"mu": mu_payload
|
|
||||||
})
|
|
||||||
|
|
||||||
# Sort by total days of DEB or Mu
|
# Sort by total days of DEB or Mu
|
||||||
accuracy_data.sort(
|
accuracy_data.sort(
|
||||||
key=lambda x: max(
|
key=lambda x: max(
|
||||||
x["deb"]["total_days"] if x["deb"] else 0,
|
x["deb"]["total_days"] if x["deb"] else 0,
|
||||||
x["mu"]["total_days"] if x["mu"] else 0
|
x["mu"]["total_days"] if x["mu"] else 0,
|
||||||
),
|
),
|
||||||
reverse=True
|
reverse=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"accuracy": accuracy_data}
|
return {"accuracy": accuracy_data}
|
||||||
@@ -891,7 +1047,9 @@ def get_ops_telegram_audit(request: Request) -> Dict[str, Any]:
|
|||||||
with db._get_connection() as conn:
|
with db._get_connection() as conn:
|
||||||
conn.row_factory = sqlite3.Row
|
conn.row_factory = sqlite3.Row
|
||||||
users_rows = conn.execute("SELECT telegram_id, username FROM users").fetchall()
|
users_rows = conn.execute("SELECT telegram_id, username FROM users").fetchall()
|
||||||
bindings_rows = conn.execute("SELECT telegram_id, supabase_user_id, supabase_email FROM supabase_bindings").fetchall()
|
bindings_rows = conn.execute(
|
||||||
|
"SELECT telegram_id, supabase_user_id, supabase_email FROM supabase_bindings"
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
user_info = {}
|
user_info = {}
|
||||||
for r in users_rows:
|
for r in users_rows:
|
||||||
@@ -901,7 +1059,7 @@ def get_ops_telegram_audit(request: Request) -> Dict[str, Any]:
|
|||||||
"username": r["username"] or f"ID: {tid}",
|
"username": r["username"] or f"ID: {tid}",
|
||||||
"supabase_user_id": None,
|
"supabase_user_id": None,
|
||||||
"supabase_email": None,
|
"supabase_email": None,
|
||||||
"is_bound": False
|
"is_bound": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
for r in bindings_rows:
|
for r in bindings_rows:
|
||||||
@@ -912,7 +1070,7 @@ def get_ops_telegram_audit(request: Request) -> Dict[str, Any]:
|
|||||||
"username": f"ID: {tid}",
|
"username": f"ID: {tid}",
|
||||||
"supabase_user_id": r["supabase_user_id"],
|
"supabase_user_id": r["supabase_user_id"],
|
||||||
"supabase_email": r["supabase_email"],
|
"supabase_email": r["supabase_email"],
|
||||||
"is_bound": True
|
"is_bound": True,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
user_info[tid]["supabase_user_id"] = r["supabase_user_id"]
|
user_info[tid]["supabase_user_id"] = r["supabase_user_id"]
|
||||||
@@ -924,13 +1082,19 @@ def get_ops_telegram_audit(request: Request) -> Dict[str, Any]:
|
|||||||
chat_ids = get_telegram_chat_ids_from_env()
|
chat_ids = get_telegram_chat_ids_from_env()
|
||||||
|
|
||||||
# Add other group IDs if configured
|
# Add other group IDs if configured
|
||||||
for env_name in ["POLYWEATHER_TELEGRAM_GROUP_ID", "POLYWEATHER_TELEGRAM_TOPICS_GROUP_ID"]:
|
for env_name in [
|
||||||
|
"POLYWEATHER_TELEGRAM_GROUP_ID",
|
||||||
|
"POLYWEATHER_TELEGRAM_TOPICS_GROUP_ID",
|
||||||
|
]:
|
||||||
val = str(os.getenv(env_name) or "").strip()
|
val = str(os.getenv(env_name) or "").strip()
|
||||||
if val and val not in chat_ids:
|
if val and val not in chat_ids:
|
||||||
chat_ids.append(val)
|
chat_ids.append(val)
|
||||||
|
|
||||||
if not bot_token or not chat_ids:
|
if not bot_token or not chat_ids:
|
||||||
return {"error": "Telegram Bot Token or Chat IDs not configured", "anomalies": []}
|
return {
|
||||||
|
"error": "Telegram Bot Token or Chat IDs not configured",
|
||||||
|
"anomalies": [],
|
||||||
|
}
|
||||||
|
|
||||||
# 3. Check membership status for all users in parallel
|
# 3. Check membership status for all users in parallel
|
||||||
results = []
|
results = []
|
||||||
@@ -940,7 +1104,7 @@ def get_ops_telegram_audit(request: Request) -> Dict[str, Any]:
|
|||||||
resp = requests.get(
|
resp = requests.get(
|
||||||
f"https://api.telegram.org/bot{bot_token}/getChatMember",
|
f"https://api.telegram.org/bot{bot_token}/getChatMember",
|
||||||
params={"chat_id": chat_id, "user_id": tg_id},
|
params={"chat_id": chat_id, "user_id": tg_id},
|
||||||
timeout=5
|
timeout=5,
|
||||||
)
|
)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
@@ -972,7 +1136,10 @@ def get_ops_telegram_audit(request: Request) -> Dict[str, Any]:
|
|||||||
valid_members = []
|
valid_members = []
|
||||||
|
|
||||||
from web.services.ops_api import legacy_routes
|
from web.services.ops_api import legacy_routes
|
||||||
active_subs = legacy_routes.SUPABASE_ENTITLEMENT.list_active_subscriptions(limit=5000)
|
|
||||||
|
active_subs = legacy_routes.SUPABASE_ENTITLEMENT.list_active_subscriptions(
|
||||||
|
limit=5000
|
||||||
|
)
|
||||||
active_subs_map = {}
|
active_subs_map = {}
|
||||||
for sub in active_subs:
|
for sub in active_subs:
|
||||||
uid = str(sub.get("user_id") or "").strip().lower()
|
uid = str(sub.get("user_id") or "").strip().lower()
|
||||||
@@ -983,16 +1150,18 @@ def get_ops_telegram_audit(request: Request) -> Dict[str, Any]:
|
|||||||
info = user_info[tg_id]
|
info = user_info[tg_id]
|
||||||
|
|
||||||
if not info["is_bound"]:
|
if not info["is_bound"]:
|
||||||
anomalies.append({
|
anomalies.append(
|
||||||
"telegram_id": tg_id,
|
{
|
||||||
"username": info["username"],
|
"telegram_id": tg_id,
|
||||||
"chat_id": chat_id,
|
"username": info["username"],
|
||||||
"status": status,
|
"chat_id": chat_id,
|
||||||
"anomaly_type": "unbound",
|
"status": status,
|
||||||
"reason": "未绑定网页账号",
|
"anomaly_type": "unbound",
|
||||||
"email": None,
|
"reason": "未绑定网页账号",
|
||||||
"expires_at": None,
|
"email": None,
|
||||||
})
|
"expires_at": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
uid = str(info["supabase_user_id"]).strip().lower()
|
uid = str(info["supabase_user_id"]).strip().lower()
|
||||||
sub = active_subs_map.get(uid)
|
sub = active_subs_map.get(uid)
|
||||||
@@ -1007,42 +1176,46 @@ def get_ops_telegram_audit(request: Request) -> Dict[str, Any]:
|
|||||||
expires_at = sub.get("expires_at")
|
expires_at = sub.get("expires_at")
|
||||||
|
|
||||||
if not sub:
|
if not sub:
|
||||||
anomalies.append({
|
anomalies.append(
|
||||||
"telegram_id": tg_id,
|
{
|
||||||
"username": info["username"],
|
"telegram_id": tg_id,
|
||||||
"chat_id": chat_id,
|
"username": info["username"],
|
||||||
"status": status,
|
"chat_id": chat_id,
|
||||||
"anomaly_type": "expired",
|
"status": status,
|
||||||
"reason": "没有有效的会员订阅",
|
"anomaly_type": "expired",
|
||||||
"email": info["supabase_email"],
|
"reason": "没有有效的会员订阅",
|
||||||
"expires_at": None,
|
"email": info["supabase_email"],
|
||||||
})
|
"expires_at": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
elif not is_paid:
|
elif not is_paid:
|
||||||
anomalies.append({
|
anomalies.append(
|
||||||
"telegram_id": tg_id,
|
{
|
||||||
"username": info["username"],
|
"telegram_id": tg_id,
|
||||||
"chat_id": chat_id,
|
"username": info["username"],
|
||||||
"status": status,
|
"chat_id": chat_id,
|
||||||
"anomaly_type": "trial_only",
|
"status": status,
|
||||||
"reason": f"仅拥有试用会员 ({plan_code})",
|
"anomaly_type": "trial_only",
|
||||||
"email": info["supabase_email"],
|
"reason": f"仅拥有试用会员 ({plan_code})",
|
||||||
"expires_at": expires_at,
|
"email": info["supabase_email"],
|
||||||
})
|
"expires_at": expires_at,
|
||||||
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
valid_members.append({
|
valid_members.append(
|
||||||
"telegram_id": tg_id,
|
{
|
||||||
"username": info["username"],
|
"telegram_id": tg_id,
|
||||||
"chat_id": chat_id,
|
"username": info["username"],
|
||||||
"status": status,
|
"chat_id": chat_id,
|
||||||
"email": info["supabase_email"],
|
"status": status,
|
||||||
"plan_code": plan_code,
|
"email": info["supabase_email"],
|
||||||
"expires_at": expires_at,
|
"plan_code": plan_code,
|
||||||
})
|
"expires_at": expires_at,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"anomalies": anomalies,
|
"anomalies": anomalies,
|
||||||
"valid_count": len(valid_members),
|
"valid_count": len(valid_members),
|
||||||
"anomaly_count": len(anomalies),
|
"anomaly_count": len(anomalies),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user