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,