Auth session 持久化 + 多分辨率跑道图表 + 拖拽缩放
- middleware: refreshMiddlewareSession 前置刷新 Supabase token - backend-auth: getUser() 优先触发 refresh,再 getSession 拿新 token - Dashboard: onAuthStateChange 实时更新 proAccess + 15min getSession 心跳 - detail proxy: 新增 city/[name]/detail 代理,透传 resolution 参数 - city_payloads: aggregate_runway_history + build_runway_band_history 跑道聚合 - chart: 10m→1m 自适应分辨率 + zoom 拖拽 + 跑道温区 Area + Runway Details 开关
This commit is contained in:
@@ -2079,11 +2079,13 @@ def _build_city_detail_payload(
|
||||
data: Dict[str, Any],
|
||||
market_slug: Optional[str] = None,
|
||||
target_date: Optional[str] = None,
|
||||
resolution: Optional[str] = "10m",
|
||||
) -> Dict[str, Any]:
|
||||
return _city_payload_detail(
|
||||
data,
|
||||
market_slug=market_slug,
|
||||
target_date=target_date,
|
||||
resolution=resolution,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ async def city_detail_aggregate(
|
||||
force_refresh: bool = False,
|
||||
market_slug: Optional[str] = None,
|
||||
target_date: Optional[str] = None,
|
||||
resolution: Optional[str] = "10m",
|
||||
):
|
||||
return await get_city_detail_aggregate_payload(
|
||||
request,
|
||||
@@ -148,6 +149,7 @@ async def city_detail_aggregate(
|
||||
force_refresh=force_refresh,
|
||||
market_slug=market_slug,
|
||||
target_date=target_date,
|
||||
resolution=resolution,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -142,6 +142,7 @@ async def get_city_detail_aggregate_payload(
|
||||
force_refresh: bool = False,
|
||||
market_slug: Optional[str] = None,
|
||||
target_date: Optional[str] = None,
|
||||
resolution: Optional[str] = "10m",
|
||||
) -> Dict[str, Any]:
|
||||
legacy_routes._assert_entitlement(request)
|
||||
city = legacy_routes._normalize_city_or_404(name)
|
||||
@@ -162,6 +163,7 @@ async def get_city_detail_aggregate_payload(
|
||||
data,
|
||||
market_slug,
|
||||
target_date,
|
||||
resolution,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, Optional, List
|
||||
from datetime import datetime, timezone
|
||||
import re
|
||||
|
||||
from web.core import _is_excluded_model_name
|
||||
|
||||
@@ -34,10 +36,134 @@ def build_city_summary_payload(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _parse_time_val(val: str) -> Optional[datetime]:
|
||||
if not val:
|
||||
return None
|
||||
try:
|
||||
val = str(val).strip().replace("Z", "+00:00")
|
||||
if "T" in val:
|
||||
return datetime.fromisoformat(val)
|
||||
else:
|
||||
return datetime.fromisoformat(val)
|
||||
except Exception:
|
||||
try:
|
||||
val_clean = re.sub(r'\.\d+', '', val)
|
||||
return datetime.strptime(val_clean, "%Y-%m-%d %H:%M:%S")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def aggregate_runway_history(raw_history: Dict[str, List[Dict[str, Any]]], resolution: str) -> Dict[str, List[Dict[str, Any]]]:
|
||||
if not raw_history:
|
||||
return {}
|
||||
if not resolution or resolution == "1m":
|
||||
return raw_history
|
||||
|
||||
try:
|
||||
if resolution.endswith("m"):
|
||||
minutes = int(resolution[:-1])
|
||||
elif resolution.endswith("h"):
|
||||
minutes = int(resolution[:-1]) * 60
|
||||
else:
|
||||
minutes = 10
|
||||
except Exception:
|
||||
minutes = 10
|
||||
|
||||
seconds = minutes * 60
|
||||
aggregated = {}
|
||||
for rwy, points in raw_history.items():
|
||||
if not points:
|
||||
continue
|
||||
|
||||
buckets = {}
|
||||
for pt in points:
|
||||
t_str = pt.get("time") or pt.get("timestamp")
|
||||
temp = pt.get("temp") or pt.get("temp_c") or pt.get("value")
|
||||
if temp is None or not isinstance(t_str, str):
|
||||
continue
|
||||
dt = _parse_time_val(t_str)
|
||||
if not dt:
|
||||
continue
|
||||
|
||||
ts = int(dt.timestamp())
|
||||
bucket_ts = (ts // seconds) * seconds
|
||||
|
||||
if bucket_ts not in buckets:
|
||||
buckets[bucket_ts] = []
|
||||
buckets[bucket_ts].append(temp)
|
||||
|
||||
bucket_points = []
|
||||
for bucket_ts in sorted(buckets.keys()):
|
||||
temps = buckets[bucket_ts]
|
||||
close_temp = temps[-1]
|
||||
bucket_dt = datetime.fromtimestamp(bucket_ts, tz=timezone.utc)
|
||||
bucket_points.append({
|
||||
"time": bucket_dt.isoformat(),
|
||||
"temp": round(close_temp, 1)
|
||||
})
|
||||
aggregated[rwy] = bucket_points
|
||||
|
||||
return aggregated
|
||||
|
||||
|
||||
def build_runway_band_history(raw_history: Dict[str, List[Dict[str, Any]]], resolution: str) -> List[Dict[str, Any]]:
|
||||
if not raw_history:
|
||||
return []
|
||||
|
||||
try:
|
||||
if resolution.endswith("m"):
|
||||
minutes = int(resolution[:-1])
|
||||
elif resolution.endswith("h"):
|
||||
minutes = int(resolution[:-1]) * 60
|
||||
else:
|
||||
minutes = 10
|
||||
except Exception:
|
||||
minutes = 10
|
||||
|
||||
seconds = minutes * 60
|
||||
buckets = {}
|
||||
for rwy, points in raw_history.items():
|
||||
for pt in points:
|
||||
t_str = pt.get("time") or pt.get("timestamp")
|
||||
temp = pt.get("temp") or pt.get("temp_c") or pt.get("value")
|
||||
if temp is None or not isinstance(t_str, str):
|
||||
continue
|
||||
dt = _parse_time_val(t_str)
|
||||
if not dt:
|
||||
continue
|
||||
|
||||
ts = int(dt.timestamp())
|
||||
bucket_ts = (ts // seconds) * seconds
|
||||
|
||||
if bucket_ts not in buckets:
|
||||
buckets[bucket_ts] = []
|
||||
buckets[bucket_ts].append(temp)
|
||||
|
||||
band_history = []
|
||||
for bucket_ts in sorted(buckets.keys()):
|
||||
temps = buckets[bucket_ts]
|
||||
if not temps:
|
||||
continue
|
||||
high_temp = max(temps)
|
||||
low_temp = min(temps)
|
||||
avg_temp = sum(temps) / len(temps)
|
||||
|
||||
bucket_dt = datetime.fromtimestamp(bucket_ts, tz=timezone.utc)
|
||||
band_history.append({
|
||||
"time": bucket_dt.isoformat(),
|
||||
"high_temp": round(high_temp, 1),
|
||||
"low_temp": round(low_temp, 1),
|
||||
"avg_temp": round(avg_temp, 1),
|
||||
})
|
||||
|
||||
return band_history
|
||||
|
||||
|
||||
def build_city_detail_payload(
|
||||
data: Dict[str, Any],
|
||||
market_slug: Optional[str] = None,
|
||||
target_date: Optional[str] = None,
|
||||
resolution: Optional[str] = "10m",
|
||||
) -> Dict[str, Any]:
|
||||
return {
|
||||
"city": data.get("name"),
|
||||
@@ -125,7 +251,8 @@ def build_city_detail_payload(
|
||||
or _build_intraday_meteorology(data),
|
||||
"vertical_profile_signal": data.get("vertical_profile_signal") or {},
|
||||
"taf": data.get("taf") or {},
|
||||
"runway_plate_history": data.get("runway_plate_history") or {},
|
||||
"runway_plate_history": aggregate_runway_history(data.get("runway_plate_history") or {}, resolution),
|
||||
"runway_band_history": build_runway_band_history(data.get("runway_plate_history") or {}, resolution),
|
||||
|
||||
"risk": data.get("risk"),
|
||||
"settlement_station": data.get("settlement_station") or {},
|
||||
|
||||
Reference in New Issue
Block a user