Tighten AMSC ops health check
This commit is contained in:
@@ -10,7 +10,16 @@ import {
|
||||
ResponsiveContainer, Cell,
|
||||
} from "recharts";
|
||||
|
||||
type ServiceResult = { ok: boolean; status?: number; latency_ms?: number; error?: string };
|
||||
type ServiceResult = {
|
||||
ok: boolean;
|
||||
status?: number;
|
||||
latency_ms?: number;
|
||||
error?: string;
|
||||
credential_configured?: boolean;
|
||||
points?: number;
|
||||
observation_time_local?: string;
|
||||
sample_city?: string;
|
||||
};
|
||||
type HealthPayload = { ok: boolean; checked_at: string; services: Record<string, ServiceResult> };
|
||||
|
||||
const LABELS: Record<string, string> = {
|
||||
@@ -135,6 +144,15 @@ export function HealthPageClient() {
|
||||
{svc.status ? <span className="text-xs text-slate-500">HTTP {svc.status}</span> : null}
|
||||
<StatusText svc={svc} />
|
||||
</div>
|
||||
{(svc.credential_configured != null || svc.points != null || svc.observation_time_local) && (
|
||||
<div className="mt-2 flex flex-wrap gap-1.5 text-[11px] text-slate-500">
|
||||
{svc.credential_configured != null && (
|
||||
<span>{svc.credential_configured ? "凭证已配置" : "凭证未配置"}</span>
|
||||
)}
|
||||
{svc.points != null && <span>跑道点 {svc.points}</span>}
|
||||
{svc.observation_time_local && <span>{svc.observation_time_local}</span>}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
from web.services import ops_api
|
||||
|
||||
|
||||
def test_ops_amsc_health_uses_configured_session_header(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
payload = {
|
||||
"code": 200,
|
||||
"data": {
|
||||
"35R/17L": {
|
||||
"RNO": "35R/17L",
|
||||
"OTIME": "2026-05-30 20:03:00",
|
||||
"TDZ_TEMP": "23.6",
|
||||
"MID_TEMP": "-",
|
||||
"END_TEMP": "23.4",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
class FakeResponse:
|
||||
ok = True
|
||||
status_code = 200
|
||||
content = b"{}"
|
||||
|
||||
def json(self):
|
||||
return payload
|
||||
|
||||
def fake_get(url, **kwargs):
|
||||
captured["url"] = url
|
||||
captured["headers"] = kwargs.get("headers") or {}
|
||||
return FakeResponse()
|
||||
|
||||
session_id = "9153$$example-session"
|
||||
monkeypatch.setenv("AMSC_AWOS_BASE_URL", "https://example.test/getWindPlate")
|
||||
monkeypatch.setenv("POLYWEATHER_AMSC_SESSION_ID", session_id)
|
||||
monkeypatch.delenv("POLYWEATHER_AMSC_COOKIE", raising=False)
|
||||
monkeypatch.setattr(ops_api._requests, "get", fake_get)
|
||||
|
||||
result = ops_api._check_amsc_awos_health(timeout=1)
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["credential_configured"] is True
|
||||
assert result["points"] == 1
|
||||
assert captured["url"].endswith("?cccc=ZSPD")
|
||||
assert captured["headers"]["sessionId"] == session_id
|
||||
assert captured["headers"]["app"] == "AMS"
|
||||
|
||||
|
||||
def test_ops_amsc_health_rejects_empty_success_response(monkeypatch):
|
||||
class FakeResponse:
|
||||
ok = True
|
||||
status_code = 200
|
||||
content = b"{}"
|
||||
|
||||
def json(self):
|
||||
return {"code": 200, "data": {}}
|
||||
|
||||
monkeypatch.setenv("AMSC_AWOS_BASE_URL", "https://example.test/getWindPlate")
|
||||
monkeypatch.setenv("POLYWEATHER_AMSC_SESSION_ID", "session")
|
||||
monkeypatch.setattr(ops_api._requests, "get", lambda *args, **kwargs: FakeResponse())
|
||||
|
||||
result = ops_api._check_amsc_awos_health(timeout=1)
|
||||
|
||||
assert result["ok"] is False
|
||||
assert result["status"] == 200
|
||||
assert result["error"] == "empty_or_unauthorized_response"
|
||||
+80
-26
@@ -510,6 +510,84 @@ def update_ops_config(request: Request, key: str, value: str) -> dict[str, Any]:
|
||||
return {"key": key, "value": value, "ok": True}
|
||||
|
||||
|
||||
def _build_amsc_awos_headers() -> dict[str, str]:
|
||||
headers = {
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": os.getenv("AMSC_AWOS_REFERER", "https://www.amsc.net.cn/"),
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36"
|
||||
),
|
||||
}
|
||||
cookie = str(os.getenv("POLYWEATHER_AMSC_COOKIE") or "").strip()
|
||||
session_id = str(os.getenv("POLYWEATHER_AMSC_SESSION_ID") or "").strip()
|
||||
if cookie:
|
||||
headers["Cookie"] = cookie
|
||||
elif session_id:
|
||||
headers["sessionId"] = session_id
|
||||
headers["app"] = "AMS"
|
||||
return headers
|
||||
|
||||
|
||||
def _check_amsc_awos_health(timeout: int = 8) -> dict[str, Any]:
|
||||
import time as _time
|
||||
|
||||
from src.data_collection.amsc_awos_sources import _amsc_parse_wind_plate_payload
|
||||
|
||||
amsc_base = str(os.getenv("AMSC_AWOS_BASE_URL") or "").strip()
|
||||
if not amsc_base:
|
||||
return {"ok": False, "error": "not configured"}
|
||||
|
||||
credential_configured = bool(
|
||||
str(os.getenv("POLYWEATHER_AMSC_COOKIE") or "").strip()
|
||||
or str(os.getenv("POLYWEATHER_AMSC_SESSION_ID") or "").strip()
|
||||
)
|
||||
try:
|
||||
t0 = _time.perf_counter()
|
||||
response = _requests.get(
|
||||
f"{amsc_base}?cccc=ZSPD",
|
||||
timeout=timeout,
|
||||
verify=False,
|
||||
headers=_build_amsc_awos_headers(),
|
||||
)
|
||||
latency_ms = round((_time.perf_counter() - t0) * 1000)
|
||||
try:
|
||||
payload = response.json() if response.content else {}
|
||||
except ValueError:
|
||||
payload = {}
|
||||
parsed = _amsc_parse_wind_plate_payload(
|
||||
payload if isinstance(payload, dict) else {},
|
||||
city_key="shanghai",
|
||||
icao="ZSPD",
|
||||
)
|
||||
points = (
|
||||
((parsed or {}).get("runway_obs") or {}).get("point_temperatures")
|
||||
if isinstance(parsed, dict)
|
||||
else []
|
||||
)
|
||||
point_count = len(points or [])
|
||||
ok = bool(response.ok and parsed and point_count > 0)
|
||||
result: dict[str, Any] = {
|
||||
"ok": ok,
|
||||
"status": response.status_code,
|
||||
"latency_ms": latency_ms,
|
||||
"credential_configured": credential_configured,
|
||||
"points": point_count,
|
||||
}
|
||||
if isinstance(parsed, dict):
|
||||
result["sample_city"] = "shanghai"
|
||||
result["observation_time_local"] = parsed.get("observation_time_local")
|
||||
if not ok:
|
||||
result["error"] = "empty_or_unauthorized_response"
|
||||
return result
|
||||
except Exception as exc:
|
||||
return {
|
||||
"ok": False,
|
||||
"credential_configured": credential_configured,
|
||||
"error": str(exc)[:100],
|
||||
}
|
||||
|
||||
|
||||
# ── Subscriptions ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1070,32 +1148,8 @@ def get_ops_health_check(request: Request) -> dict[str, Any]:
|
||||
except Exception as e:
|
||||
results["amos"] = {"ok": False, "error": str(e)[:100]}
|
||||
|
||||
# AMSC AWOS (China mainland airports) — matches actual source SSL + URL pattern
|
||||
amsc_base = str(os.getenv("AMSC_AWOS_BASE_URL") or "").strip()
|
||||
if amsc_base:
|
||||
try:
|
||||
t0 = _time.perf_counter()
|
||||
r = _r.get(
|
||||
f"{amsc_base}?cccc=ZSPD",
|
||||
timeout=timeout,
|
||||
verify=False,
|
||||
headers={
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Referer": os.getenv(
|
||||
"AMSC_AWOS_REFERER", "https://www.amsc.net.cn/"
|
||||
),
|
||||
"User-Agent": "PolyWeather/1.0",
|
||||
},
|
||||
)
|
||||
results["amsc_awos"] = {
|
||||
"ok": r.ok,
|
||||
"status": r.status_code,
|
||||
"latency_ms": round((_time.perf_counter() - t0) * 1000),
|
||||
}
|
||||
except Exception as e:
|
||||
results["amsc_awos"] = {"ok": False, "error": str(e)[:100]}
|
||||
else:
|
||||
results["amsc_awos"] = {"ok": False, "error": "not configured"}
|
||||
# AMSC AWOS (China mainland airports)
|
||||
results["amsc_awos"] = _check_amsc_awos_health(timeout=timeout)
|
||||
|
||||
# NOAA WRH (US settlement verification)
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user