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(