diff --git a/frontend/hooks/useLeafletMap.ts b/frontend/hooks/useLeafletMap.ts index 3bd8b202..17f13a7a 100644 --- a/frontend/hooks/useLeafletMap.ts +++ b/frontend/hooks/useLeafletMap.ts @@ -92,6 +92,8 @@ function buildNearbyIconHtml(detail: CityDetail, station: NearbyStation) { ? String(rawLabel).replace(/\s*\(NMC\)$/i, "区域实况 (NMC)") : rawLabel; let windHtml = ""; + const windDirectionText = String(station.wind_direction_text || "").trim(); + const windPowerText = String(station.wind_power_text || "").trim(); if (station.wind_dir != null) { const rotation = (Number(station.wind_dir) + 180) % 360; @@ -103,6 +105,13 @@ function buildNearbyIconHtml(detail: CityDetail, station: NearbyStation) { ${speed} `; + } else if (windDirectionText || windPowerText) { + const windText = [windDirectionText, windPowerText].filter(Boolean).join(" "); + windHtml = ` +
+ ${windText} +
+ `; } return ` diff --git a/frontend/lib/dashboard-types.ts b/frontend/lib/dashboard-types.ts index e0b9dee2..ac4a6657 100644 --- a/frontend/lib/dashboard-types.ts +++ b/frontend/lib/dashboard-types.ts @@ -117,6 +117,8 @@ export interface NearbyStation { is_official?: boolean; is_airport_station?: boolean; is_settlement_anchor?: boolean; + wind_direction_text?: string | null; + wind_power_text?: string | null; } export interface HourlyTrendPoint { diff --git a/frontend/lib/dashboard-utils.ts b/frontend/lib/dashboard-utils.ts index d3cd2353..e09e2dfa 100644 --- a/frontend/lib/dashboard-utils.ts +++ b/frontend/lib/dashboard-utils.ts @@ -1870,8 +1870,8 @@ export function computeFrontTrendSignal( : "云量回落且温度抬升,白天增温效率在改善。"; } return isEnglish(locale) - ? "Read cloud-cover change together with temperature, dew point, wind, and precipitation; cloud change alone does not define the regime." - : "云量变化需要结合温度、露点、风向和降水一起看,不能单独决定天气形势。"; + ? "Read forecast cloud-cover increase together with temperature, dew point, wind, and precipitation; it does not override the current observed sky condition." + : "这里显示的是预测窗口内的云量增幅,需要结合温度、露点、风向和降水一起看,不能覆盖当前实况的天空状况。"; })(); const dewNote = (() => { if (dewDelta >= 1.2 && tempDelta >= 0.8) { @@ -1982,7 +1982,7 @@ export function computeFrontTrendSignal( value: `${Math.round(precipMax)}%`, }, { - label: isEnglish(locale) ? "Cloud-cover delta" : "云量变化", + label: isEnglish(locale) ? "Forecast cloud-cover delta" : "预测云量增幅", note: cloudNote, tone: cloudDelta >= 15 && tempDelta >= 0 diff --git a/scripts/grant_subscription_by_email.py b/scripts/grant_subscription_by_email.py index 4467cd4d..e5ba62c7 100644 --- a/scripts/grant_subscription_by_email.py +++ b/scripts/grant_subscription_by_email.py @@ -14,22 +14,47 @@ if PROJECT_ROOT not in sys.path: sys.path.insert(0, PROJECT_ROOT) +def _select_exact_user_id(payload: object, email: str) -> str: + from src.payments.contract_checkout import PaymentCheckoutError + + normalized_email = str(email or "").strip().lower() + users = payload.get("users") if isinstance(payload, dict) else None + if not isinstance(users, list) or not users: + raise PaymentCheckoutError(404, f"supabase user not found for email={email}") + + matches = [] + for row in users: + if not isinstance(row, dict): + continue + row_email = str(row.get("email") or "").strip().lower() + user_id = str(row.get("id") or "").strip() + if row_email == normalized_email and user_id: + matches.append(user_id) + + unique_matches = [] + for user_id in matches: + if user_id not in unique_matches: + unique_matches.append(user_id) + + if len(unique_matches) == 1: + return unique_matches[0] + if len(unique_matches) > 1: + raise PaymentCheckoutError( + 409, + f"multiple exact supabase users matched email={email}: {unique_matches}", + ) + raise PaymentCheckoutError(404, f"exact supabase user not found for email={email}") + + def _lookup_user_id_by_email(email: str) -> str: - from src.payments.contract_checkout import PAYMENT_CHECKOUT, PaymentCheckoutError + from src.payments.contract_checkout import PAYMENT_CHECKOUT payload = PAYMENT_CHECKOUT._auth_admin_request( # noqa: SLF001 "GET", f"/admin/users?email={email}", allowed_status=[200], ) - users = payload.get("users") if isinstance(payload, dict) else None - if not isinstance(users, list) or not users: - raise PaymentCheckoutError(404, f"supabase user not found for email={email}") - user = users[0] if isinstance(users[0], dict) else {} - user_id = str(user.get("id") or "").strip() - if not user_id: - raise PaymentCheckoutError(404, f"supabase user id missing for email={email}") - return user_id + return _select_exact_user_id(payload, email) def main() -> int: diff --git a/tests/test_grant_subscription_by_email.py b/tests/test_grant_subscription_by_email.py new file mode 100644 index 00000000..96a764ed --- /dev/null +++ b/tests/test_grant_subscription_by_email.py @@ -0,0 +1,30 @@ +from scripts.grant_subscription_by_email import _select_exact_user_id +from src.payments.contract_checkout import PaymentCheckoutError + + +def test_select_exact_user_id_prefers_exact_email_match(): + payload = { + "users": [ + {"id": "wrong-user", "email": "louischanre+alias@gmail.com"}, + {"id": "right-user", "email": "louischanre@gmail.com"}, + ] + } + + result = _select_exact_user_id(payload, "louischanre@gmail.com") + + assert result == "right-user" + + +def test_select_exact_user_id_raises_when_exact_email_missing(): + payload = { + "users": [ + {"id": "wrong-user", "email": "louischanre+alias@gmail.com"}, + ] + } + + try: + _select_exact_user_id(payload, "louischanre@gmail.com") + except PaymentCheckoutError as exc: + assert exc.status_code == 404 + else: + raise AssertionError("expected PaymentCheckoutError")