From 5228d2b3fdc65e3d339ce3f0fa0319f7707716b8 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 29 May 2026 20:46:35 +0800 Subject: [PATCH] Update landing and weather observation policies --- frontend/app/page.tsx | 25 +- ...temperatureDefaultVisibilityPolicy.test.ts | 74 ++- .../scan-terminal/temperature-chart-logic.ts | 26 +- .../landing/InstitutionalLandingPage.tsx | 575 ++++++++++-------- .../__tests__/landingPricingReferral.test.ts | 11 + src/data_collection/country_networks.py | 5 +- src/data_collection/weather_sources.py | 12 + tests/test_country_networks.py | 20 + 8 files changed, 469 insertions(+), 279 deletions(-) diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 3ea56d6a..1b5757d7 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -42,12 +42,25 @@ export default async function HomePage({ url: "https://polyweather.top", applicationCategory: "BusinessApplication", operatingSystem: "Web", - offers: { - "@type": "Offer", - price: "10.00", - priceCurrency: "USD", - description: "Monthly subscription", - }, + offers: [ + { + "@type": "Offer", + name: "Pro monthly", + price: "29.90", + priceCurrency: "USD", + description: + "Pro subscription for 30 days. Referral users pay 26.90 USD-equivalent USDC for the first month.", + availability: "https://schema.org/InStock", + }, + { + "@type": "Offer", + name: "Pro quarterly", + price: "79.90", + priceCurrency: "USD", + description: "Pro subscription for 90 days.", + availability: "https://schema.org/InStock", + }, + ], }; return ( diff --git a/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts index 5441ce50..38cd7ce4 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts @@ -192,8 +192,34 @@ export function runTests() { "settlement/HKO observations should be visible by default", ); assert( - __isTemperatureSeriesVisibleByDefaultForTest("guangzhou", "metar"), - "METAR observations should be visible by default", + !__isTemperatureSeriesVisibleByDefaultForTest("guangzhou", "metar"), + "METAR observations should be hidden by default outside Hong Kong and Shenzhen", + ); + assert( + !activeDefaultSeries.some((item) => item.key === "metar"), + "non-Hong Kong/Shenzhen airport METAR observations should not affect the active chart series by default", + ); + assert( + __getVisibleTemperatureSeriesForTest("guangzhou", series, { metar: true }).some( + (item) => item.key === "metar", + ), + "users should still be able to enable the hidden airport METAR series from the legend", + ); + assert( + __isTemperatureSeriesVisibleByDefaultForTest("hong kong", "metar"), + "Hong Kong METAR/HKO observations should remain visible by default", + ); + assert( + __isTemperatureSeriesVisibleByDefaultForTest("Lau Fau Shan", "madis"), + "Lau Fau Shan HKO observations should remain visible by default", + ); + assert( + __isTemperatureSeriesVisibleByDefaultForTest("shenzhen", "madis"), + "Shenzhen HKO observations should remain visible by default", + ); + assert( + !__isTemperatureSeriesVisibleByDefaultForTest("new york", "madis"), + "non-Hong Kong/Shenzhen airport-primary observations should be hidden by default", ); assert( !__isTemperatureSeriesVisibleByDefaultForTest("guangzhou", "model_curve_ECMWF"), @@ -1162,6 +1188,50 @@ export function runTests() { "long-lived chart with only one fresh observation should keep a renderable current reference line instead of an invisible single-point series", ); + const istanbulMgmOnlySeries = __buildTemperatureChartDataForTest( + { + city: "istanbul", + local_date: "2026-05-29", + local_time: "15:10", + tz_offset_seconds: 3 * 60 * 60, + current_temp: 18, + current_max_so_far: 18, + airport: "LTFM", + } as any, + { + localTime: "15:10", + times: [], + temps: [], + airportPrimary: { + source_code: "mgm", + source_label: "MGM", + temp: 18.2, + obs_time: "2026-05-29T12:10:00Z", + }, + airportCurrent: { + source_code: "metar", + source_label: "METAR", + temp: 17, + obs_time: "14:50", + }, + airportPrimaryTodayObs: [ + { time: "2026-05-29T12:00:00Z", temp: 18 }, + { time: "2026-05-29T12:05:00Z", temp: 18.1 }, + ], + } as any, + "1D", + ); + const istanbulMgmSeries = seriesByKey(istanbulMgmOnlySeries.series as any, "madis") as any; + assert(istanbulMgmSeries?.label === "MGM", "Istanbul airport-primary series should be labeled MGM"); + assert( + istanbulMgmSeries.values.some((value: number | null) => value === 18.2), + "MGM series should append the latest MGM airport-primary observation", + ); + assert( + !istanbulMgmSeries.values.some((value: number | null) => value === 17), + "MGM series should not mix METAR airportCurrent points into the MGM airport-station line", + ); + const chengduMergedHourly = __mergePatchIntoHourlyForTest( { localTime: "05:25", diff --git a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts index d0ddcf67..809d1178 100644 --- a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts +++ b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts @@ -86,6 +86,10 @@ function isTemperatureSeriesVisibleByDefault(city: string, seriesKey: string) { if (seriesKey.startsWith("model_curve_")) { return normalizeCityKey(city) === "paris" && seriesKey === "model_curve_AROME HD"; } + if (seriesKey === "metar" || seriesKey === "madis") { + const cityKey = normalizeCityKey(city); + return cityKey === "hongkong" || cityKey === "laufaushan" || cityKey === "shenzhen"; + } return true; } @@ -606,6 +610,24 @@ function appendLatestAirportObservation( return merged; } +function isMgmAirportPrimary(hourly: HourlyForecast) { + const primary = hourly?.airportPrimary; + const sourceTokens = [ + primary?.source_code, + primary?.source_label, + (primary as any)?.source, + ].map((value) => String(value || "").toLowerCase()); + return sourceTokens.some((value) => value === "mgm" || value.includes("turkey_mgm")); +} + +function airportPrimaryObservationPoints(hourly: HourlyForecast) { + return appendLatestAirportObservation( + hourly?.airportPrimaryTodayObs, + hourly?.airportPrimary, + ...(isMgmAirportPrimary(hourly) ? [] : [hourly?.airportCurrent]), + ); +} + function seriesStats(values: Array) { const nums = values.filter((v): v is number => validNumber(v) !== null); const latest = nums.length ? nums[nums.length - 1] : null; @@ -680,7 +702,7 @@ function getObservationDisplayMetrics( const settlementObs = normObs(hourly?.settlementTodayObs || row?.settlement_today_obs || row?.metar_context?.settlement_today_obs, tzOffset, MAX_OBS_POINTS, localDateStr); const metarObs = normObs(hourly?.metarTodayObs || row?.metar_today_obs || row?.metar_context?.today_obs || row?.metar_recent_obs || row?.metar_context?.recent_obs, tzOffset, MAX_OBS_POINTS, localDateStr); const madisObs = normObs( - appendLatestAirportObservation(hourly?.airportPrimaryTodayObs, hourly?.airportPrimary, hourly?.airportCurrent), + airportPrimaryObservationPoints(hourly), tzOffset, MAX_OBS_POINTS, localDateStr, @@ -1611,7 +1633,7 @@ function buildFullDayChartData( ); const madisObs = filterTimelinePointsToLocalDay( normObs( - appendLatestAirportObservation(hourly?.airportPrimaryTodayObs, hourly?.airportPrimary, hourly?.airportCurrent), + airportPrimaryObservationPoints(hourly), tzOffset, MAX_OBS_POINTS, localDateStr, diff --git a/frontend/components/landing/InstitutionalLandingPage.tsx b/frontend/components/landing/InstitutionalLandingPage.tsx index 6c75b97c..6f5baafd 100644 --- a/frontend/components/landing/InstitutionalLandingPage.tsx +++ b/frontend/components/landing/InstitutionalLandingPage.tsx @@ -4,13 +4,17 @@ import { useEffect, useState } from "react"; import Link from "next/link"; import { ArrowRight, + Bell, Check, CheckCircle2, + Clock3, + CloudSun, + Database, Gauge, LineChart, - LockKeyhole, Radar, ShieldCheck, + Users, } from "lucide-react"; import { I18nProvider, useI18n } from "@/hooks/useI18n"; import { @@ -23,7 +27,7 @@ const COVERAGE_EN = [ "DEB blend forecast", "Model-implied distribution", "Intraday observation windows", - "AI weather evidence", + "Deviation checks and risk thresholds", "Paid Telegram alerts", ]; @@ -32,30 +36,79 @@ const COVERAGE_ZH = [ "DEB 智能融合预报", "模型隐含分布预测", "日内分段观测窗口", - "AI 气象证据链解读", - "付费电报实时通知", + "偏差校验与风控阈值", + "付费 Telegram 实时通知", ]; const PRO_FEATURES_EN = [ - "Real-time METAR observations & runway sensor data", - "Real-time METAR observations & alerts", - "DEB blend forecast model", - "Model-implied distribution analysis", - "Intraday observation windows & deviation metrics", - "Paid Telegram alerts & Webhook API", - "24/7 priority professional support", + "METAR airport observations and runway-level reference data", + "DEB blend forecast with model-spread context", + "Model-implied distribution and probability estimates", + "Intraday windows, deviation metrics, and settlement context", + "Paid Telegram group eligibility and alert workflows", + "Priority support for subscription and access issues", ]; const PRO_FEATURES_ZH = [ - "实时 METAR 机场实测与跑道传感器数据", - "实时 METAR 机场实测与预警", - "DEB 智能融合预测模型", - "模型隐含分布预测与估算", - "日内观测窗口与偏差度量指标", - "付费电报群通知与 API 接口推送", - "7×24小时专业技术与客服支持", + "METAR 机场实测与跑道级参考数据", + "DEB 智能融合预报与模型分歧背景", + "模型隐含分布预测与概率估算", + "日内观测窗口、偏差度量与结算背景", + "付费 Telegram 群准入与提醒工作流", + "订阅与准入问题优先支持", ]; +function WeatherWorkflowIllustration({ isEn }: { isEn: boolean }) { + const cards = isEn + ? [ + { label: "Airport obs", value: "24.2C", color: "bg-emerald-100 text-emerald-800" }, + { label: "DEB forecast", value: "25.0C", color: "bg-sky-100 text-sky-800" }, + { label: "Risk", value: "Watch", color: "bg-amber-100 text-amber-800" }, + ] + : [ + { label: "机场实测", value: "24.2C", color: "bg-emerald-100 text-emerald-800" }, + { label: "DEB 预报", value: "25.0C", color: "bg-sky-100 text-sky-800" }, + { label: "风险状态", value: "观察", color: "bg-amber-100 text-amber-800" }, + ]; + + return ( +