diff --git a/frontend/components/dashboard/ScanTerminalCard.module.css b/frontend/components/dashboard/ScanTerminalCard.module.css
index 847f910a..7abedcc0 100644
--- a/frontend/components/dashboard/ScanTerminalCard.module.css
+++ b/frontend/components/dashboard/ScanTerminalCard.module.css
@@ -1225,3 +1225,67 @@
font-weight: 800;
line-height: 1.5;
}
+
+/* ── AMOS runway observation panel ── */
+.root :global(.scan-amos-runway-panel) {
+ margin-top: 14px;
+ border: 1px solid rgba(77, 163, 255, 0.12);
+ border-radius: 16px;
+ background: rgba(11, 18, 32, 0.24);
+ padding: 14px;
+}
+
+.root :global(.scan-amos-source-tag) {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 1px 7px;
+ border-radius: 999px;
+ background: rgba(77, 163, 255, 0.12);
+ color: var(--color-accent-secondary);
+ font-size: 10px;
+ font-weight: 700;
+}
+
+.root :global(.scan-amos-runway-grid) {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 10px;
+ margin-top: 10px;
+}
+
+.root :global(.scan-amos-runway-card) {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 10px 12px;
+ border: 1px solid rgba(77, 163, 255, 0.08);
+ border-radius: 12px;
+ background: rgba(11, 18, 32, 0.38);
+}
+
+.root :global(.scan-amos-runway-label) {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--color-accent-secondary);
+ letter-spacing: 0.02em;
+}
+
+.root :global(.scan-amos-runway-temp) {
+ font-size: 20px;
+ font-weight: 800;
+ font-variant-numeric: tabular-nums;
+ color: var(--color-text-primary);
+ line-height: 1.15;
+}
+
+.root :global(.scan-amos-runway-temp small) {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+}
+
+.root :global(.scan-amos-runway-detail) {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--color-text-muted);
+}
diff --git a/frontend/components/dashboard/ScanTerminalLightTheme.module.css b/frontend/components/dashboard/ScanTerminalLightTheme.module.css
index 5c2ef36c..d9422462 100644
--- a/frontend/components/dashboard/ScanTerminalLightTheme.module.css
+++ b/frontend/components/dashboard/ScanTerminalLightTheme.module.css
@@ -982,3 +982,31 @@ html.light .root :global(.scan-terminal.light .scan-retry-button) {
html.light .root :global(.scan-terminal.light .scan-retry-button:hover) {
background: rgba(37, 99, 235, 0.14);
}
+
+/* ── AMOS runway panel light overrides ── */
+html.light .root :global(.scan-terminal.light .scan-amos-runway-panel) {
+ background: rgba(37, 99, 235, 0.03);
+ border-color: rgba(37, 99, 235, 0.12);
+}
+
+html.light .root :global(.scan-terminal.light .scan-amos-runway-card) {
+ background: #ffffff;
+ border-color: rgba(37, 99, 235, 0.08);
+}
+
+html.light .root :global(.scan-terminal.light .scan-amos-runway-label) {
+ color: #2563EB;
+}
+
+html.light .root :global(.scan-terminal.light .scan-amos-runway-temp) {
+ color: #0F172A;
+}
+
+html.light .root :global(.scan-terminal.light .scan-amos-source-tag) {
+ background: rgba(37, 99, 235, 0.08);
+ color: #2563EB;
+}
+
+html.light .root :global(.scan-terminal.light .scan-amos-runway-detail) {
+ color: #64748B;
+}
diff --git a/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx b/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx
index 16c589db..0aaa23d0 100644
--- a/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx
+++ b/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx
@@ -5,6 +5,7 @@ import type { MouseEvent } from "react";
import { useEffect, useState } from "react";
import { AiCityTemperatureChart } from "@/components/dashboard/scan-terminal/AiCityTemperatureChart";
import { AiEvidencePanel } from "@/components/dashboard/scan-terminal/AiEvidencePanel";
+import { AmosRunwayPanel } from "@/components/dashboard/scan-terminal/AmosRunwayPanel";
import { CityCardHeader } from "@/components/dashboard/scan-terminal/CityCardHeader";
import { MobileDecisionCard } from "@/components/dashboard/scan-terminal/MobileDecisionCard";
import { ModelEvidencePanel } from "@/components/dashboard/scan-terminal/ModelEvidencePanel";
@@ -650,6 +651,14 @@ export function AiPinnedCityCard({
/>
+ {detail?.amos ? (
+
+ ) : null}
+
) : !detail ? (
diff --git a/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx b/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx
new file mode 100644
index 00000000..837f8ec4
--- /dev/null
+++ b/frontend/components/dashboard/scan-terminal/AmosRunwayPanel.tsx
@@ -0,0 +1,86 @@
+"use client";
+
+import type { AmosData } from "@/lib/dashboard-types";
+
+function runwayTempClass(temp: number | null | undefined): string {
+ if (temp == null || !Number.isFinite(temp)) return "";
+ if (temp >= 40) return "temp-extreme-hot";
+ if (temp <= -5) return "temp-extreme-cold";
+ return "";
+}
+
+export function AmosRunwayPanel({
+ amos,
+ isEn,
+ tempSymbol,
+}: {
+ amos: AmosData;
+ isEn: boolean;
+ tempSymbol: string;
+}) {
+ const runwayPairs = amos.runway_obs?.runway_pairs;
+ const runwayTemps = amos.runway_obs?.temperatures ?? amos.runway_temps;
+ const runwayWinds = amos.runway_obs?.wind_speeds;
+ const runwayVis = amos.runway_obs?.visibility_mor;
+ const runwayRvr = amos.runway_obs?.rvr;
+
+ if (!runwayPairs || !runwayTemps || runwayPairs.length === 0) return null;
+
+ const pairs = runwayPairs.slice(0, runwayTemps.length);
+
+ return (
+
+
+ {isEn ? "Runway Observations" : "跑道实测"} · {amos.station_label || amos.icao || ""}
+
+ {amos.temp_source === "metar"
+ ? isEn ? "Official METAR" : "官方 METAR"
+ : isEn ? "Runway median" : "跑道中位数"}
+
+
+
+ {pairs.map(([rwyA, rwyB], idx) => {
+ const temps = runwayTemps[idx];
+ const wind = runwayWinds?.[idx];
+ const vis = runwayVis?.[idx];
+ const rvr = runwayRvr?.[idx];
+ return (
+
+
+ {rwyA}/{rwyB}
+
+ {temps ? (
+
+ {temps[0].toFixed(1)}{tempSymbol}
+ {temps[1] != null ? (
+
+ {isEn ? " Dew " : " 露点 "}
+ {temps[1].toFixed(1)}{tempSymbol}
+
+ ) : null}
+
+ ) : null}
+ {vis != null && vis > 0 ? (
+
+ {isEn ? "Vis " : "能见度 "}
+ {vis >= 10000 ? (isEn ? "≥10km" : "≥10公里") : `${vis}m`}
+
+ ) : null}
+ {rvr != null && rvr > 0 ? (
+
+ RVR {rvr >= 2000 ? "≥2000m" : `${rvr}m`}
+
+ ) : null}
+ {wind ? (
+
+ {wind[0].toFixed(1)}kt
+ {wind[1] != null ? ` (${wind[1].toFixed(1)}–${wind[2].toFixed(1)})` : ""}
+
+ ) : null}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/frontend/lib/dashboard-types.ts b/frontend/lib/dashboard-types.ts
index e54edf4a..88525375 100644
--- a/frontend/lib/dashboard-types.ts
+++ b/frontend/lib/dashboard-types.ts
@@ -910,6 +910,36 @@ export interface CityDetail {
source_forecasts?: SourceForecasts;
market_scan?: MarketScan;
intraday_meteorology?: IntradayMeteorology;
+ amos?: AmosData | null;
+}
+
+export interface AmosData {
+ temp?: number | null;
+ temp_c?: number | null;
+ dew?: number | null;
+ dew_c?: number | null;
+ pressure_hpa?: number | null;
+ wind_kt?: number | null;
+ temp_source?: string | null;
+ runway_temps?: Array<[number, number]> | null;
+ source?: string | null;
+ source_label?: string | null;
+ icao?: string | null;
+ station_label?: string | null;
+ raw_metar?: string | null;
+ raw_taf?: string | null;
+ runway_obs?: {
+ runway_pairs?: Array<[string, string]> | null;
+ temperatures?: Array<[number, number]> | null;
+ pressures_hpa?: number[] | null;
+ wind_directions?: Array<[number, number, number]> | null;
+ wind_speeds?: Array<[number, number, number]> | null;
+ visibility_mor?: number[] | null;
+ rvr?: number[] | null;
+ } | null;
+ observation_source?: string | null;
+ observation_source_zh?: string | null;
+ observation_time?: string | null;
}
export interface HistoryPoint {
diff --git a/grep.exe.stackdump b/grep.exe.stackdump
new file mode 100644
index 00000000..4831183d
--- /dev/null
+++ b/grep.exe.stackdump
@@ -0,0 +1,15 @@
+Stack trace:
+Frame Function Args
+000FFFFBD10 00180062F57 (000FFFFBF18, 00000000002, 00000000002, 000FFFFDE50)
+00000000000 00180065045 (00000000064, 00000000000, 00000000190, 00000000000)
+000FFFFC420 001801369F8 (00000000000, 00000000000, 00000000000, 00000000000)
+000000000C1 0018013202B (00000000000, 00000000000, 00000000000, 00000000000)
+000FFFFC840 00180132435 (001803551C0, 000FFFFCAB0, 000000000B5, 00100437000)
+000FFFFC840 001802195E8 (001803551C0, 001803552B8, 00000000000, 000FFFFC8C0)
+000FFFFC840 00100429F25 (001800DB6FE, 001800435C6, 000FFFFC940, 00000000000)
+000FFFFC8B0 00100404B11 (0000000000B, 000FFFFCDF0, 0018005DAAF, 00000000000)
+000FFFFCAA0 001004294B0 (000FFFFCC40, 00000000000, 00000000000, 000FFFFCD30)
+000FFFFCD30 00180049B91 (00000000000, 00000000000, 00000000000, 00000000000)
+000FFFFFFF0 00180047716 (00000000000, 00000000000, 00000000000, 00000000000)
+000FFFFFFF0 001800477C4 (00000000000, 00000000000, 00000000000, 00000000000)
+End of stack trace