Tighten AMSC ops health check

This commit is contained in:
2569718930@qq.com
2026-05-30 20:10:14 +08:00
parent 9bcfd9d1eb
commit e00405df8a
3 changed files with 165 additions and 27 deletions
@@ -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>
))}
+66
View File
@@ -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
View File
@@ -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: