diff --git a/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx b/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx index 837f8ec4..7720fdd3 100644 --- a/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx +++ b/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx @@ -24,9 +24,14 @@ export function AmosRunwayPanel({ const runwayVis = amos.runway_obs?.visibility_mor; const runwayRvr = amos.runway_obs?.rvr; - if (!runwayPairs || !runwayTemps || runwayPairs.length === 0) return null; + if (!runwayPairs || runwayPairs.length === 0) return null; - const pairs = runwayPairs.slice(0, runwayTemps.length); + const maxItems = Math.max( + runwayPairs.length, + runwayTemps?.length || 0, + runwayWinds?.length || 0, + ); + const pairs = runwayPairs.slice(0, maxItems); return (
@@ -49,17 +54,15 @@ export function AmosRunwayPanel({
{rwyA}/{rwyB}
- {temps ? ( -
- {temps[0].toFixed(1)}{tempSymbol} - {temps[1] != null ? ( - - {isEn ? " Dew " : " 露点 "} - {temps[1].toFixed(1)}{tempSymbol} - - ) : null} -
- ) : null} +
+ {temps?.[0] != null ? `${temps[0].toFixed(1)}${tempSymbol}` : "--"} + {temps?.[1] != null ? ( + + {isEn ? " Dew " : " 露点 "} + {temps[1].toFixed(1)}{tempSymbol} + + ) : null} +
{vis != null && vis > 0 ? (
{isEn ? "Vis " : "能见度 "} diff --git a/src/data_collection/amos_station_sources.py b/src/data_collection/amos_station_sources.py index 6c760220..6cad1f76 100644 --- a/src/data_collection/amos_station_sources.py +++ b/src/data_collection/amos_station_sources.py @@ -136,43 +136,76 @@ class AmosStationSourceMixin: amos_cache_ttl_sec: int = 300 # 5 minutes def _amos_get_page(self, icao: str) -> Optional[str]: - """Fetch the AMOS page for a given ICAO code.""" + """Fetch the AMOS page for a given ICAO code. + + The AMOS site uses JavaScript airport switching. We try multiple + approaches: GET with parameters, POST with form data, and cookie replay. + """ started = time.perf_counter() + station_id = AMOS_STATION_IDS.get(icao, "") + airport_label_ko = { + "RKSI": "인천", + "RKPK": "김해", + }.get(icao, "") + + fetch_impl = None + if hasattr(self, "session") and hasattr(self.session, "get"): + def _get(url: str) -> str: + resp = self.session.get(url, timeout=float(getattr(self, "timeout", 4.0))) + resp.raise_for_status() + return resp.text + + def _post(url: str, data: dict) -> str: + resp = self.session.post(url, data=data, timeout=float(getattr(self, "timeout", 4.0))) + resp.raise_for_status() + return resp.text + fetch_impl = (_get, _post) + else: + getter = getattr(self, "_http_get_text", None) + if not callable(getter): + return None + def _get(url: str) -> str: + return str(getter(url)) + def _post(url: str, data: dict) -> str: + return str(getter(url)) + fetch_impl = (_get, _post) + + http_get, http_post = fetch_impl + try: - # Try multiple URL patterns - urls = [ - f"{AMOS_BASE_URL}?icao={icao}", - f"{AMOS_BASE_URL}?stn={AMOS_STATION_IDS.get(icao, '')}", - AMOS_BASE_URL, # default page (usually RKSI) - ] - for url in urls: + # Strategy 1: GET first to establish session, then POST to switch + _ = http_get(AMOS_BASE_URL) + for form_data in [ + {"icao": icao, "stn": station_id, "airport": airport_label_ko}, + {"stnId": station_id}, + {"code": icao}, + {"selectAirport": icao}, + ]: try: - getter = getattr(self, "_http_get_text", None) - if callable(getter): - text = str(getter(url)) - else: - if not hasattr(self, "session"): - break - response = self.session.get( - url, timeout=float(getattr(self, "timeout", 4.0)) - ) - response.raise_for_status() - text = response.text - if text and icao in text.upper(): - record_source_call( - "amos", "page", "success", - (time.perf_counter() - started) * 1000.0, - ) + text = http_post(AMOS_BASE_URL, form_data) + if text and (icao in text.upper() or airport_label_ko in text): + record_source_call("amos", "page", "success", (time.perf_counter() - started) * 1000.0) return text except Exception: continue + + # Strategy 2: GET with query parameters + for url in [ + f"{AMOS_BASE_URL}?icao={icao}", + f"{AMOS_BASE_URL}?stn={station_id}", + ]: + try: + text = http_get(url) + if text and (icao in text.upper() or airport_label_ko in text): + record_source_call("amos", "page", "success", (time.perf_counter() - started) * 1000.0) + return text + except Exception: + continue + return None except Exception as exc: logger.debug("AMOS page fetch failed icao={}: {}", icao, exc) - record_source_call( - "amos", "page", "error", - (time.perf_counter() - started) * 1000.0, - ) + record_source_call("amos", "page", "error", (time.perf_counter() - started) * 1000.0) return None def fetch_amos_official_current( diff --git a/web/analysis_service.py b/web/analysis_service.py index 0769d9a3..22135048 100644 --- a/web/analysis_service.py +++ b/web/analysis_service.py @@ -2487,7 +2487,7 @@ def _analyze( "mgm": mgm_data, "mgm_nearby": raw.get("mgm_nearby", []), "nearby_source": raw.get("nearby_source") or ("mgm" if city.lower() in TURKISH_MGM_CITIES else "metar_cluster"), - "amos": amos_data if amos_data.get("temp_c") is not None else None, + "amos": amos_data if amos_data and (amos_data.get("temp_c") is not None or amos_data.get("runway_obs") is not None) else None, "forecast": { "today_high": om_today, "daily": forecast_daily,