From fbbd13b1c3282d3a670ff386f3e2959f33137e35 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 19 Apr 2026 02:38:34 +0800 Subject: [PATCH] Add nearby station timing sync labels --- .../components/dashboard/Dashboard.module.css | 21 +++ .../dashboard/FutureForecastModal.tsx | 24 +++- frontend/hooks/useLeafletMap.ts | 53 ++++++++ frontend/lib/dashboard-types.ts | 10 ++ frontend/lib/dashboard-utils.ts | 39 +++++- src/data_collection/country_networks.py | 127 +++++++++++++++++- src/data_collection/metar_sources.py | 1 + src/data_collection/mgm_sources.py | 9 +- tests/test_country_networks.py | 41 ++++++ web/analysis_service.py | 22 ++- 10 files changed, 328 insertions(+), 19 deletions(-) diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index 757c3ef8..d32cf99c 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -2158,6 +2158,27 @@ color: var(--text-muted); } +.root :global(.nearby-time) { + display: flex; + align-items: center; + gap: 4px; + margin-top: 2px; + font-size: 8px; + font-weight: 700; + color: rgba(148, 163, 184, 0.88); + white-space: nowrap; +} + +.root :global(.nearby-time span + span)::before { + content: "·"; + margin-right: 4px; + color: rgba(148, 163, 184, 0.55); +} + +.root :global(.nearby-time.is-stale) { + color: #fbbf24; +} + .root :global(.nearby-wind) { display: flex; align-items: center; diff --git a/frontend/components/dashboard/FutureForecastModal.tsx b/frontend/components/dashboard/FutureForecastModal.tsx index 1b651ad3..d1d946c5 100644 --- a/frontend/components/dashboard/FutureForecastModal.tsx +++ b/frontend/components/dashboard/FutureForecastModal.tsx @@ -866,6 +866,22 @@ export function FutureForecastModal() { const leaderLabel = String(leadSignal?.leader_station_label || "").trim() || String(leadSignal?.leader_station_code || "").trim(); + const leaderSyncStatus = String(leadSignal?.leader_sync_status || "").trim().toLowerCase(); + const leaderSyncDelta = Number(leadSignal?.leader_time_delta_vs_anchor_minutes); + const syncNote = + leaderSyncStatus === "near_realtime" || leaderSyncStatus === "lagged" + ? Number.isFinite(leaderSyncDelta) + ? locale === "en-US" + ? ` Timing offset versus the airport anchor is about ${Math.round(leaderSyncDelta)} minutes.` + : ` 与机场锚点存在约 ${Math.round(leaderSyncDelta)} 分钟时间差。` + : locale === "en-US" + ? " Nearby observations are not fully synchronized." + : " 周边观测并非完全同步。" + : leaderSyncStatus === "unknown" + ? locale === "en-US" + ? " Nearby station timing is not fully verified." + : " 周边站观测时间尚未完全校验。" + : ""; const absDelta = Math.abs(delta); const status = delta <= -0.4 @@ -884,12 +900,12 @@ export function FutureForecastModal() { const note = delta <= -0.4 ? locale === "en-US" - ? `Airport anchor is ${absDelta.toFixed(1)}${detail.temp_symbol} cooler than the nearby official network${leaderLabel ? `, led by ${leaderLabel}` : ""}.` - : `机场主站当前比周边官方站网低 ${absDelta.toFixed(1)}${detail.temp_symbol}${leaderLabel ? `,领先点位是 ${leaderLabel}` : ""}。` + ? `Airport anchor is ${absDelta.toFixed(1)}${detail.temp_symbol} cooler than the nearby official network${leaderLabel ? `, led by ${leaderLabel}` : ""}.${syncNote}` + : `机场主站当前比周边官方站网低 ${absDelta.toFixed(1)}${detail.temp_symbol}${leaderLabel ? `,领先点位是 ${leaderLabel}` : ""}。${syncNote}` : delta >= 0.4 ? locale === "en-US" - ? `Airport anchor is ${absDelta.toFixed(1)}${detail.temp_symbol} hotter than the nearby official network.` - : `机场主站当前比周边官方站网高 ${absDelta.toFixed(1)}${detail.temp_symbol}。` + ? `Airport anchor is ${absDelta.toFixed(1)}${detail.temp_symbol} hotter than the nearby official network.${syncNote}` + : `机场主站当前比周边官方站网高 ${absDelta.toFixed(1)}${detail.temp_symbol}。${syncNote}` : locale === "en-US" ? "Airport anchor and nearby official network are broadly aligned." : "机场主站与周边官方站网当前大体齐平。"; diff --git a/frontend/hooks/useLeafletMap.ts b/frontend/hooks/useLeafletMap.ts index e8d98e72..c151902a 100644 --- a/frontend/hooks/useLeafletMap.ts +++ b/frontend/hooks/useLeafletMap.ts @@ -134,7 +134,59 @@ function buildNearbyIconHtml(detail: CityDetail, station: NearbyStation) { if (!text || text === "9999") return ""; return text; }; + const isEnglishUi = + typeof document !== "undefined" && + String(document.documentElement.lang || "").toLowerCase().startsWith("en"); const symbol = detail.temp_symbol || "°C"; + const formatObsTime = () => { + const label = String(station.obs_time_label || "").trim(); + if (label) return label; + const raw = String(station.obs_time || "").trim(); + if (!raw) return ""; + if (raw.endsWith("Z") || raw.includes("+00:00")) { + const parsed = new Date(raw); + if (!Number.isNaN(parsed.getTime())) { + return `${String(parsed.getUTCHours()).padStart(2, "0")}:${String(parsed.getUTCMinutes()).padStart(2, "0")}Z`; + } + } + if (raw.includes("T")) return raw.split("T").pop()?.slice(0, 5) || ""; + if (raw.includes(" ")) return raw.split(" ").pop()?.slice(0, 5) || ""; + return raw.slice(0, 5); + }; + const syncLabel = () => { + const status = String(station.sync_status || "").trim().toLowerCase(); + const delta = Number(station.time_delta_vs_anchor_minutes); + if (status === "synced") return isEnglishUi ? "synced" : "同步"; + if (status === "near_realtime") { + return Number.isFinite(delta) + ? isEnglishUi + ? `${Math.round(delta)}m off` + : `差${Math.round(delta)}m` + : isEnglishUi + ? "near-live" + : "近实时"; + } + if (status === "lagged") { + return Number.isFinite(delta) + ? isEnglishUi + ? `${Math.round(delta)}m lag` + : `滞后${Math.round(delta)}m` + : isEnglishUi + ? "lagged" + : "滞后"; + } + if (status === "stale") return isEnglishUi ? "stale" : "过期"; + return ""; + }; + const obsTime = formatObsTime(); + const syncText = syncLabel(); + const timingHtml = + obsTime || syncText + ? `
+ ${obsTime ? `${obsTime}` : ""} + ${syncText ? `${syncText}` : ""} +
` + : ""; const rawLabel = station.station_label || station.name || @@ -182,6 +234,7 @@ function buildNearbyIconHtml(detail: CityDetail, station: NearbyStation) { ${station.temp ?? "--"} ${symbol} + ${timingHtml} ${windHtml} diff --git a/frontend/lib/dashboard-types.ts b/frontend/lib/dashboard-types.ts index 93488365..d23acac4 100644 --- a/frontend/lib/dashboard-types.ts +++ b/frontend/lib/dashboard-types.ts @@ -117,6 +117,12 @@ export interface NearbyStation { is_official?: boolean; is_airport_station?: boolean; is_settlement_anchor?: boolean; + obs_time?: string | null; + obs_time_label?: string | null; + age_minutes?: number | null; + time_delta_vs_anchor_minutes?: number | null; + sync_status?: "synced" | "near_realtime" | "lagged" | "stale" | "unknown" | string | null; + usable_for_intraday?: boolean; wind_direction_text?: string | null; wind_power_text?: string | null; } @@ -413,6 +419,10 @@ export interface CityDetail { leader_station_code?: string | null; leader_station_label?: string | null; leader_temp?: number | null; + leader_obs_time?: string | null; + leader_obs_time_label?: string | null; + leader_sync_status?: string | null; + leader_time_delta_vs_anchor_minutes?: number | null; }; network_spread_signal?: { available?: boolean; diff --git a/frontend/lib/dashboard-utils.ts b/frontend/lib/dashboard-utils.ts index 6f9fae59..727e1acd 100644 --- a/frontend/lib/dashboard-utils.ts +++ b/frontend/lib/dashboard-utils.ts @@ -2786,7 +2786,11 @@ export function getShortTermNowcastLines( const recent = Array.isArray(detail.metar_recent_obs) ? detail.metar_recent_obs.slice(-4) : []; - const nearby = Array.isArray(detail.mgm_nearby) ? detail.mgm_nearby : []; + const nearby = Array.isArray(detail.official_nearby) && detail.official_nearby.length + ? detail.official_nearby + : Array.isArray(detail.mgm_nearby) + ? detail.mgm_nearby + : []; const nearbySource = String(detail.nearby_source || "").toLowerCase(); const sourceLabel = nearbySource === "mgm" || isTurkishMgmCity(detail) @@ -2809,12 +2813,33 @@ export function getShortTermNowcastLines( Number.isFinite(currentTemp) && Number.isFinite(baseline) ? currentTemp - baseline : 0; - let nearbyLead: { diff: number; name: string; temp: number } | null = null; + let nearbyLead: { diff: number; name: string; temp: number; syncText: string } | null = null; for (const station of nearby) { const temp = Number(station.temp); + if (station.usable_for_intraday === false) continue; if (!Number.isFinite(temp) || !Number.isFinite(currentTemp)) continue; const diff = temp - currentTemp; + const syncStatus = String(station.sync_status || "").toLowerCase(); + const syncDelta = Number(station.time_delta_vs_anchor_minutes); + const syncText = + syncStatus === "synced" + ? isEnglish(locale) + ? "time-synced" + : "时间同步" + : syncStatus === "near_realtime" || syncStatus === "lagged" + ? Number.isFinite(syncDelta) + ? isEnglish(locale) + ? `${Math.round(syncDelta)} min offset` + : `时间差 ${Math.round(syncDelta)} 分钟` + : isEnglish(locale) + ? "not fully synchronized" + : "非完全同步" + : syncStatus === "unknown" + ? isEnglish(locale) + ? "timing unverified" + : "时间待校验" + : ""; if (!nearbyLead || Math.abs(diff) > Math.abs(nearbyLead.diff)) { nearbyLead = { diff, @@ -2823,9 +2848,11 @@ export function getShortTermNowcastLines( station.icao || (isEnglish(locale) ? "Nearby station" : "周边站"), temp, + syncText, }; } } + const usableNearbyCount = nearby.filter((station) => station.usable_for_intraday !== false).length; const rows: Array = [ [ @@ -2845,8 +2872,8 @@ export function getShortTermNowcastLines( [ sourceLabel, isEnglish(locale) - ? `${nearby.length} stations joined the nearby scan.` - : `${nearby.length} 个站点参与邻近监控。`, + ? `${usableNearbyCount}/${nearby.length} stations usable for the nearby scan; station timestamps may differ.` + : `${usableNearbyCount}/${nearby.length} 个站点可参与邻近监控;周边站观测时间可能不同步。`, ], ]; @@ -2865,8 +2892,8 @@ export function getShortTermNowcastLines( rows.push([ isEnglish(locale) ? "Leading station" : "领先站", isEnglish(locale) - ? `${nearbyLead.name} ${nearbyLead.temp}${detail.temp_symbol}, relative to primary station ${formatDelta(nearbyLead.diff, detail.temp_symbol)} (${tone}).` - : `${nearbyLead.name} ${nearbyLead.temp}${detail.temp_symbol},相对主站 ${formatDelta(nearbyLead.diff, detail.temp_symbol)}(${tone})。`, + ? `${nearbyLead.name} ${nearbyLead.temp}${detail.temp_symbol}, relative to primary station ${formatDelta(nearbyLead.diff, detail.temp_symbol)} (${tone})${nearbyLead.syncText ? `; ${nearbyLead.syncText}` : ""}.` + : `${nearbyLead.name} ${nearbyLead.temp}${detail.temp_symbol},相对主站 ${formatDelta(nearbyLead.diff, detail.temp_symbol)}(${tone})${nearbyLead.syncText ? `;${nearbyLead.syncText}` : ""}。`, ]); } diff --git a/src/data_collection/country_networks.py b/src/data_collection/country_networks.py index fcaf6315..b1397963 100644 --- a/src/data_collection/country_networks.py +++ b/src/data_collection/country_networks.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import datetime, timezone from typing import Any, Dict, List, Optional from src.data_collection.city_registry import CITY_REGISTRY @@ -30,6 +31,106 @@ def _safe_float(value: Any) -> Optional[float]: return None +def _parse_obs_datetime(value: Any, epoch_value: Any = None) -> Optional[datetime]: + for candidate in (epoch_value, value): + if candidate is None or candidate == "": + continue + try: + if isinstance(candidate, (int, float)): + numeric = float(candidate) + if numeric > 1_000_000_000_000: + numeric /= 1000.0 + if numeric > 1_000_000_000: + return datetime.fromtimestamp(numeric, tz=timezone.utc) + text = str(candidate).strip() + if not text: + continue + if text.isdigit(): + numeric = float(text) + if numeric > 1_000_000_000_000: + numeric /= 1000.0 + if numeric > 1_000_000_000: + return datetime.fromtimestamp(numeric, tz=timezone.utc) + normalized = text.replace(" ", "T") + if normalized.endswith("Z"): + normalized = normalized[:-1] + "+00:00" + return datetime.fromisoformat(normalized) + except Exception: + continue + return None + + +def _format_obs_time_label(value: Any, epoch_value: Any = None) -> Optional[str]: + text = str(value or "").strip() + parsed = _parse_obs_datetime(value, epoch_value) + if parsed is not None: + suffix = "Z" if parsed.tzinfo is not None and parsed.utcoffset() == timezone.utc.utcoffset(parsed) else "" + return parsed.strftime("%H:%M") + suffix + if not text: + return None + if "T" in text: + return text.split("T")[-1][:5] + if " " in text: + return text.split()[-1][:5] + return text[:5] if len(text) >= 5 else text + + +def _timing_delta_minutes(anchor_dt: Optional[datetime], station_dt: Optional[datetime]) -> Optional[int]: + if anchor_dt is None or station_dt is None: + return None + try: + if (anchor_dt.tzinfo is None) != (station_dt.tzinfo is None): + return None + delta = abs((station_dt - anchor_dt).total_seconds()) / 60.0 + return int(round(delta)) + except Exception: + return None + + +def _station_age_minutes(station_dt: Optional[datetime]) -> Optional[int]: + if station_dt is None or station_dt.tzinfo is None: + return None + try: + return int(round((datetime.now(timezone.utc) - station_dt.astimezone(timezone.utc)).total_seconds() / 60.0)) + except Exception: + return None + + +def _sync_status(delta_minutes: Optional[int], age_minutes: Optional[int]) -> str: + reference = delta_minutes if delta_minutes is not None else age_minutes + if reference is None: + return "unknown" + if reference <= 10: + return "synced" + if reference <= 30: + return "near_realtime" + if reference <= 60: + return "lagged" + return "stale" + + +def _enrich_station_timing( + anchor: Optional[Dict[str, Any]], + rows: List[Dict[str, Any]], +) -> List[Dict[str, Any]]: + anchor = anchor or {} + anchor_dt = _parse_obs_datetime(anchor.get("obs_time"), anchor.get("obs_time_epoch")) + enriched: List[Dict[str, Any]] = [] + for row in rows: + station_dt = _parse_obs_datetime(row.get("obs_time"), row.get("obs_time_epoch")) + delta_minutes = _timing_delta_minutes(anchor_dt, station_dt) + age_minutes = _station_age_minutes(station_dt) + status = _sync_status(delta_minutes, age_minutes) + enriched_row = dict(row) + enriched_row["obs_time_label"] = _format_obs_time_label(row.get("obs_time"), row.get("obs_time_epoch")) + enriched_row["age_minutes"] = age_minutes + enriched_row["time_delta_vs_anchor_minutes"] = delta_minutes + enriched_row["sync_status"] = status + enriched_row["usable_for_intraday"] = status != "stale" + enriched.append(enriched_row) + return enriched + + def _city_meta(city: str) -> Dict[str, Any]: return CITY_REGISTRY.get(str(city or "").strip().lower(), {}) or {} @@ -145,6 +246,7 @@ def _metar_cluster_rows(raw: Dict[str, Any]) -> List[Dict[str, Any]]: is_airport_station=True, is_settlement_anchor=False, extra={ + "obs_time_epoch": row.get("obs_time_epoch"), "wind_dir": _safe_float(row.get("wind_dir")), "wind_speed_kt": _safe_float(row.get("wind_speed_kt") or row.get("wind_speed")), "raw_metar": row.get("raw_metar"), @@ -284,6 +386,7 @@ def _mgm_rows(raw: Dict[str, Any], city: str) -> List[Dict[str, Any]]: temp=row.get("temp"), lat=row.get("lat"), lon=row.get("lon"), + obs_time=row.get("obs_time"), source_code="mgm", source_label="MGM", is_official=True, @@ -368,7 +471,11 @@ def _network_signals( official_rows: List[Dict[str, Any]], ) -> Dict[str, Any]: airport_temp = _safe_float((airport_primary or {}).get("temp")) - valid_rows = [row for row in official_rows if _safe_float(row.get("temp")) is not None] + valid_rows = [ + row + for row in official_rows + if _safe_float(row.get("temp")) is not None and row.get("usable_for_intraday") is not False + ] if not valid_rows: return { "network_lead_signal": {"available": False}, @@ -394,6 +501,10 @@ def _network_signals( "leader_station_code": hottest.get("station_code"), "leader_station_label": hottest.get("station_label"), "leader_temp": hottest_temp, + "leader_obs_time": hottest.get("obs_time"), + "leader_obs_time_label": hottest.get("obs_time_label"), + "leader_sync_status": hottest.get("sync_status"), + "leader_time_delta_vs_anchor_minutes": hottest.get("time_delta_vs_anchor_minutes"), }, "network_spread_signal": { "available": spread is not None, @@ -588,9 +699,21 @@ def build_country_network_snapshot(city: str, raw: Dict[str, Any]) -> Dict[str, provider = get_country_network_provider(city) metadata = provider.settlement_station_metadata(city) airport_primary = provider.airport_primary_current(city, raw) or {} - official_nearby = provider.official_nearby_current(city, raw) + official_nearby = _enrich_station_timing( + airport_primary, + provider.official_nearby_current(city, raw), + ) status = provider.official_network_status(city, raw) signals = _network_signals(airport_primary, official_nearby) + usable_count = len([row for row in official_nearby if row.get("usable_for_intraday") is not False]) + stale_count = len([row for row in official_nearby if row.get("sync_status") == "stale"]) + unknown_count = len([row for row in official_nearby if row.get("sync_status") == "unknown"]) + status = { + **status, + "usable_row_count": usable_count, + "stale_row_count": stale_count, + "unknown_timing_count": unknown_count, + } return { "provider_code": provider.provider_code, "provider_label": provider.provider_label, diff --git a/src/data_collection/metar_sources.py b/src/data_collection/metar_sources.py index 23210051..bf8e50fa 100644 --- a/src/data_collection/metar_sources.py +++ b/src/data_collection/metar_sources.py @@ -446,6 +446,7 @@ class MetarSourceMixin: "wind_speed": obs.get("wspd"), "wind_speed_kt": obs.get("wspd"), "obs_time": obs.get("obsTime") or obs.get("reportTime"), + "obs_time_epoch": obs.get("obsTime"), "raw_metar": obs.get("rawOb"), } ) diff --git a/src/data_collection/mgm_sources.py b/src/data_collection/mgm_sources.py index b5000e35..447815ee 100644 --- a/src/data_collection/mgm_sources.py +++ b/src/data_collection/mgm_sources.py @@ -305,8 +305,14 @@ class MgmSourceMixin: temp = obs.get("sicaklik") wind_speed = obs.get("ruzgarHiz") wind_dir = obs.get("ruzgarYon") + obs_time = obs.get("veriZamani") if temp is not None and temp > -9000: - return ist_no, {"temp": temp, "wind_speed": wind_speed, "wind_dir": wind_dir} + return ist_no, { + "temp": temp, + "wind_speed": wind_speed, + "wind_dir": wind_dir, + "obs_time": obs_time, + } except Exception: pass return None, None @@ -344,6 +350,7 @@ class MgmSourceMixin: "lat": lat, "lon": lon, "temp": temp.get("temp") if isinstance(temp, dict) else temp, + "obs_time": temp.get("obs_time") if isinstance(temp, dict) else None, "wind_speed": temp.get("wind_speed") if isinstance(temp, dict) else None, "wind_dir": temp.get("wind_dir") if isinstance(temp, dict) else None, "istNo": ist_no diff --git a/tests/test_country_networks.py b/tests/test_country_networks.py index b98a477a..e51d8490 100644 --- a/tests/test_country_networks.py +++ b/tests/test_country_networks.py @@ -48,6 +48,7 @@ def test_turkey_mgm_provider_returns_official_nearby_rows(): "lat": 40.1, "lon": 32.9, "temp": 17.1, + "obs_time": "2026-04-06T10:08:00.000Z", } ], } @@ -58,6 +59,46 @@ def test_turkey_mgm_provider_returns_official_nearby_rows(): assert snapshot["official_network_status"]["available"] is True assert snapshot["official_nearby"][0]["source_code"] == "mgm" assert snapshot["official_nearby"][0]["is_official"] is True + assert snapshot["official_nearby"][0]["time_delta_vs_anchor_minutes"] == 8 + assert snapshot["official_nearby"][0]["sync_status"] == "synced" + assert snapshot["official_nearby"][0]["usable_for_intraday"] is True + assert snapshot["official_network_status"]["usable_row_count"] == 1 + + +def test_nearby_station_timing_marks_stale_rows_unusable_for_network_signal(): + raw = { + "metar": { + "observation_time": "2026-04-06T10:00:00.000Z", + "current": {"temp": 20.0}, + }, + "mgm_nearby": [ + { + "name": "Fresh", + "istNo": "100", + "lat": 40.1, + "lon": 32.9, + "temp": 21.0, + "obs_time": "2026-04-06T10:20:00.000Z", + }, + { + "name": "Stale Hot", + "istNo": "101", + "lat": 40.2, + "lon": 33.0, + "temp": 26.0, + "obs_time": "2026-04-06T08:30:00.000Z", + }, + ], + } + + snapshot = build_country_network_snapshot("ankara", raw) + + stale = next(row for row in snapshot["official_nearby"] if row["station_label"] == "Stale Hot") + assert stale["sync_status"] == "stale" + assert stale["usable_for_intraday"] is False + assert snapshot["official_network_status"]["stale_row_count"] == 1 + assert snapshot["network_lead_signal"]["leader_station_label"] == "Fresh" + assert snapshot["airport_vs_network_delta"] == 1.0 def test_china_provider_falls_back_to_metar_cluster_without_replacing_airport_anchor(): diff --git a/web/analysis_service.py b/web/analysis_service.py index 0408365f..908f2251 100644 --- a/web/analysis_service.py +++ b/web/analysis_service.py @@ -1349,6 +1349,16 @@ def _build_intraday_meteorology(data: Dict[str, Any]) -> Dict[str, Any]: if airport_delta is not None: available_layers += 1 leader = str(lead_signal.get("leader_station_label") or lead_signal.get("leader_station_code") or "").strip() + sync_status = str(lead_signal.get("leader_sync_status") or "").strip().lower() + sync_delta = _sf(lead_signal.get("leader_time_delta_vs_anchor_minutes")) + sync_suffix_zh = "" + sync_suffix_en = "" + if sync_status in {"near_realtime", "lagged"} and sync_delta is not None: + sync_suffix_zh = f";但与机场锚点约差 {sync_delta:.0f} 分钟,作为降权信号处理" + sync_suffix_en = f"; timing differs from the airport anchor by about {sync_delta:.0f} minutes, so this signal is down-weighted" + elif sync_status == "unknown": + sync_suffix_zh = ";周边站观测时间不可完全校验,作为弱参考" + sync_suffix_en = "; station timing is not fully verified, so this is treated as a weak reference" if airport_delta <= -0.4: support_score += 1 _add_signal( @@ -1356,9 +1366,9 @@ def _build_intraday_meteorology(data: Dict[str, Any]) -> Dict[str, Any]: label="站网对比", label_en="Station-network comparison", direction="support", - strength="medium", - summary=f"周边站网较机场锚点偏热 {abs(airport_delta):.1f}{unit}{f',领先点位 {leader}' if leader else ''}。", - summary_en=f"Nearby stations are {abs(airport_delta):.1f}{unit} warmer than the airport anchor{f'; leading site: {leader}' if leader else ''}.", + strength="weak" if sync_suffix_zh else "medium", + summary=f"周边站网较机场锚点偏热 {abs(airport_delta):.1f}{unit}{f',领先点位 {leader}' if leader else ''}{sync_suffix_zh}。", + summary_en=f"Nearby stations are {abs(airport_delta):.1f}{unit} warmer than the airport anchor{f'; leading site: {leader}' if leader else ''}{sync_suffix_en}.", ) elif airport_delta >= 0.4: suppress_score += 1 @@ -1367,9 +1377,9 @@ def _build_intraday_meteorology(data: Dict[str, Any]) -> Dict[str, Any]: label="站网对比", label_en="Station-network comparison", direction="suppress", - strength="medium", - summary=f"机场锚点较周边站网偏热 {abs(airport_delta):.1f}{unit},继续上修需要机场自身后续报文确认。", - summary_en=f"The airport anchor is {abs(airport_delta):.1f}{unit} warmer than nearby stations; further upside needs confirmation from later airport reports.", + strength="weak" if sync_suffix_zh else "medium", + summary=f"机场锚点较周边站网偏热 {abs(airport_delta):.1f}{unit},继续上修需要机场自身后续报文确认{sync_suffix_zh}。", + summary_en=f"The airport anchor is {abs(airport_delta):.1f}{unit} warmer than nearby stations; further upside needs confirmation from later airport reports{sync_suffix_en}.", ) else: _add_signal(