feat: implement Telegram push utility and add new dashboard components for scan terminal and paywall management
This commit is contained in:
@@ -5,7 +5,6 @@ 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";
|
||||
@@ -388,11 +387,9 @@ export function AiPinnedCityCard({
|
||||
decisionExpectedHighNumber != null
|
||||
? formatTemperatureValue(decisionExpectedHighNumber, tempSymbol, { digits: 1 })
|
||||
: "--";
|
||||
const amosRange = detail?.amos?.runway_temp_range;
|
||||
const observedLabel = amosRange ? (isEn ? "Runway" : "跑道实况") : undefined;
|
||||
const currentTempText = amosRange
|
||||
? `${amosRange[0].toFixed(1)}~${amosRange[1].toFixed(1)}${tempSymbol}`
|
||||
: currentTempNumber != null
|
||||
const observedLabel = undefined;
|
||||
const currentTempText =
|
||||
currentTempNumber != null
|
||||
? formatTemperatureValue(currentTempNumber, tempSymbol, { digits: 1 })
|
||||
: "--";
|
||||
const debText =
|
||||
@@ -655,15 +652,6 @@ export function AiPinnedCityCard({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(detail?.name === "seoul" || detail?.name === "busan") ? (
|
||||
<AmosRunwayPanel
|
||||
amos={detail?.amos}
|
||||
isEn={isEn}
|
||||
tempSymbol={tempSymbol}
|
||||
airportCurrent={detail?.airport_current ?? null}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<ModelEvidencePanel detail={detail} isEn={isEn} />
|
||||
</div>
|
||||
) : !detail ? (
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
"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,
|
||||
airportCurrent,
|
||||
}: {
|
||||
amos?: AmosData | null;
|
||||
isEn: boolean;
|
||||
tempSymbol: string;
|
||||
airportCurrent?: {
|
||||
temp?: number | null;
|
||||
wind_speed_kt?: number | null;
|
||||
wind_dir?: number | null;
|
||||
pressure_hpa?: number | null;
|
||||
visibility_mi?: number | null;
|
||||
raw_metar?: string | null;
|
||||
source_label?: string | null;
|
||||
obs_time?: string | null;
|
||||
stale_for_today?: boolean | null;
|
||||
} | null;
|
||||
}) {
|
||||
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;
|
||||
const hasAmosRunway = runwayPairs && runwayPairs.length > 0;
|
||||
|
||||
// Fallback: single-card METAR observation for non-AMOS airports
|
||||
if (!hasAmosRunway) {
|
||||
if (airportCurrent?.temp == null && airportCurrent?.wind_speed_kt == null) return null;
|
||||
const sourceLabel = airportCurrent?.source_label || (isEn ? "METAR" : "机场报文");
|
||||
const staleNote = airportCurrent?.stale_for_today
|
||||
? isEn ? " (stale)" : "(过旧)"
|
||||
: "";
|
||||
return (
|
||||
<div className="scan-amos-runway-panel">
|
||||
<div className="scan-ai-city-section-title">
|
||||
{isEn ? "Airport Observation" : "机场观测"} · {sourceLabel}{staleNote}
|
||||
</div>
|
||||
<div className="scan-amos-runway-grid scan-amos-single">
|
||||
<div className="scan-amos-runway-card">
|
||||
<div className={`scan-amos-runway-temp ${runwayTempClass(airportCurrent.temp)}`}>
|
||||
{airportCurrent.temp != null ? `${airportCurrent.temp.toFixed(1)}${tempSymbol}` : "--"}
|
||||
</div>
|
||||
{airportCurrent.wind_speed_kt != null ? (
|
||||
<div className="scan-amos-runway-detail">
|
||||
{isEn ? "Wind " : "风 "}
|
||||
{airportCurrent.wind_dir != null ? `${airportCurrent.wind_dir}° ` : ""}
|
||||
{airportCurrent.wind_speed_kt}kt
|
||||
</div>
|
||||
) : null}
|
||||
{airportCurrent.pressure_hpa != null ? (
|
||||
<div className="scan-amos-runway-detail">
|
||||
QNH {airportCurrent.pressure_hpa.toFixed(1)} hPa
|
||||
</div>
|
||||
) : null}
|
||||
{airportCurrent.visibility_mi != null ? (
|
||||
<div className="scan-amos-runway-detail">
|
||||
{isEn ? "Vis " : "能见度 "}
|
||||
{(airportCurrent.visibility_mi * 1609).toFixed(0)}m
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!runwayPairs || runwayPairs.length === 0) return null;
|
||||
|
||||
const maxItems = Math.max(
|
||||
runwayPairs.length,
|
||||
runwayTemps?.length ?? 0,
|
||||
runwayWinds?.length ?? 0,
|
||||
);
|
||||
const pairs = runwayPairs.slice(0, maxItems);
|
||||
|
||||
return (
|
||||
<div className="scan-amos-runway-panel">
|
||||
<div className="scan-ai-city-section-title">
|
||||
{isEn ? "Runway Observations" : "跑道实测"} · {amos.station_label || amos.icao || ""}
|
||||
<span className="scan-amos-source-tag">
|
||||
{amos.temp_source === "metar"
|
||||
? isEn ? "Official METAR" : "官方 METAR"
|
||||
: isEn ? "Runway median" : "跑道中位数"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="scan-amos-runway-grid">
|
||||
{pairs.map(([rwyA, rwyB], idx) => {
|
||||
const temps = runwayTemps?.[idx];
|
||||
const wind = runwayWinds?.[idx];
|
||||
const vis = runwayVis?.[idx];
|
||||
const rvr = runwayRvr?.[idx];
|
||||
return (
|
||||
<div key={`${rwyA}/${rwyB}`} className="scan-amos-runway-card">
|
||||
<div className="scan-amos-runway-label">
|
||||
{rwyA}/{rwyB}
|
||||
</div>
|
||||
<div className={`scan-amos-runway-temp ${runwayTempClass(temps?.[0])}`}>
|
||||
{temps?.[0] != null ? `${temps[0].toFixed(1)}${tempSymbol}` : "--"}
|
||||
{temps?.[1] != null ? (
|
||||
<small>
|
||||
{isEn ? " Dew " : " 露点 "}
|
||||
{temps[1].toFixed(1)}{tempSymbol}
|
||||
</small>
|
||||
) : null}
|
||||
</div>
|
||||
{vis != null && vis > 0 ? (
|
||||
<div className="scan-amos-runway-detail">
|
||||
{isEn ? "Vis " : "能见度 "}
|
||||
{vis >= 10000 ? (isEn ? "≥10km" : "≥10公里") : `${vis}m`}
|
||||
</div>
|
||||
) : null}
|
||||
{rvr != null && rvr > 0 ? (
|
||||
<div className="scan-amos-runway-detail">
|
||||
RVR {rvr >= 2000 ? "≥2000m" : `${rvr}m`}
|
||||
</div>
|
||||
) : null}
|
||||
{wind ? (
|
||||
<div className="scan-amos-runway-detail">
|
||||
{wind[0].toFixed(1)}kt
|
||||
{wind[1] != null ? ` (${wind[1].toFixed(1)}–${wind[2].toFixed(1)})` : ""}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { useCityDetails, useDashboardActions } from "@/hooks/useDashboardStore";
|
||||
import { useI18n } from "@/hooks/useI18n";
|
||||
import type { CityDetail } from "@/lib/dashboard-types";
|
||||
|
||||
const RUNWAY_OBSERVATION_CITIES = [
|
||||
{ key: "seoul", zh: "首尔", en: "Seoul", icao: "RKSI", sourceLabel: "AMOS" },
|
||||
{ key: "busan", zh: "釜山", en: "Busan", icao: "RKPK", sourceLabel: "AMOS" },
|
||||
{ key: "beijing", zh: "北京", en: "Beijing", icao: "ZBAA", sourceLabel: "AMSC AWOS" },
|
||||
{ key: "shanghai", zh: "上海", en: "Shanghai", icao: "ZSPD", sourceLabel: "AMSC AWOS" },
|
||||
{ key: "guangzhou", zh: "广州", en: "Guangzhou", icao: "ZGGG", sourceLabel: "AMSC AWOS" },
|
||||
{ key: "shenzhen", zh: "深圳", en: "Shenzhen", icao: "ZGSZ", sourceLabel: "AMSC AWOS" },
|
||||
{ key: "qingdao", zh: "青岛", en: "Qingdao", icao: "ZSQD", sourceLabel: "AMSC AWOS" },
|
||||
{ key: "chengdu", zh: "成都", en: "Chengdu", icao: "ZUUU", sourceLabel: "AMSC AWOS" },
|
||||
{ key: "chongqing", zh: "重庆", en: "Chongqing", icao: "ZUCK", sourceLabel: "AMSC AWOS" },
|
||||
{ key: "wuhan", zh: "武汉", en: "Wuhan", icao: "ZHHH", sourceLabel: "AMSC AWOS" },
|
||||
] as const;
|
||||
|
||||
function formatTemp(value: number | null | undefined, symbol = "°C") {
|
||||
return value == null || !Number.isFinite(Number(value))
|
||||
? "-"
|
||||
: `${Number(value).toFixed(1)}${symbol}`;
|
||||
}
|
||||
|
||||
function getRunwayRows(detail?: CityDetail | null) {
|
||||
return detail?.amos?.runway_obs?.point_temperatures ?? [];
|
||||
}
|
||||
|
||||
function getRunwayPairRows(detail?: CityDetail | null) {
|
||||
const runway_pairs = detail?.amos?.runway_obs?.runway_pairs ?? [];
|
||||
const runway_temps =
|
||||
detail?.amos?.runway_obs?.temperatures ??
|
||||
detail?.amos?.runway_temps ??
|
||||
[];
|
||||
return runway_pairs.map(([from, to], index) => ({
|
||||
label: `${from}/${to}`,
|
||||
temp: runway_temps[index]?.[0] ?? null,
|
||||
dew: runway_temps[index]?.[1] ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
function sourceIsAmsc(detail?: CityDetail | null) {
|
||||
return detail?.amos?.source === "amsc_awos";
|
||||
}
|
||||
|
||||
function RunwayCityCard({
|
||||
detail,
|
||||
isEn,
|
||||
label,
|
||||
}: {
|
||||
detail?: CityDetail | null;
|
||||
isEn: boolean;
|
||||
label: (typeof RUNWAY_OBSERVATION_CITIES)[number];
|
||||
}) {
|
||||
const rows = getRunwayRows(detail);
|
||||
const pairRows = getRunwayPairRows(detail);
|
||||
const tempSymbol = detail?.temp_symbol || "°C";
|
||||
const range = detail?.amos?.runway_temp_range;
|
||||
const obsLocal =
|
||||
detail?.amos?.observation_time_local || detail?.amos?.observation_time;
|
||||
const hasPointRows = sourceIsAmsc(detail) && rows.length > 0;
|
||||
const hasPairRows = pairRows.some((row) => row.temp != null || row.dew != null);
|
||||
const hasRunwayData = hasPointRows || hasPairRows;
|
||||
|
||||
return (
|
||||
<article className="monitor-card">
|
||||
<div className="monitor-card-head">
|
||||
<span className="monitor-city-name">{isEn ? label.en : label.zh}</span>
|
||||
<span className="monitor-airport-name">/ {label.icao}</span>
|
||||
<span className="monitor-obs-time">{obsLocal || "--"}</span>
|
||||
</div>
|
||||
|
||||
<div className="monitor-stats">
|
||||
<div className="monitor-high-row">
|
||||
<span className="monitor-stat-label">{label.sourceLabel}</span>
|
||||
<span className="monitor-high-value">
|
||||
{range
|
||||
? `${range[0].toFixed(1)}–${range[1].toFixed(1)}${tempSymbol}`
|
||||
: hasPairRows
|
||||
? pairRows
|
||||
.map((row) => row.temp)
|
||||
.filter((value): value is number => value != null)
|
||||
.map((value) => value.toFixed(1))
|
||||
.join(" / ") + tempSymbol
|
||||
: "--"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasRunwayData ? (
|
||||
<>
|
||||
<div className="monitor-divider" />
|
||||
{hasPointRows ? (
|
||||
<>
|
||||
<div className="monitor-rw-row">
|
||||
<span className="monitor-rw-label">{isEn ? "Runway" : "跑道"}</span>
|
||||
<span className="monitor-rw-temp">TDZ / MID / END</span>
|
||||
</div>
|
||||
{rows.map((row) => (
|
||||
<div
|
||||
key={row.runway || `${row.tdz_temp}-${row.end_temp}`}
|
||||
className="monitor-rw-row"
|
||||
>
|
||||
<span className="monitor-rw-label">{row.runway || "--"}</span>
|
||||
<span className="monitor-rw-temp">
|
||||
{formatTemp(row.tdz_temp, tempSymbol)} /{" "}
|
||||
{formatTemp(row.mid_temp, tempSymbol)} /{" "}
|
||||
{formatTemp(row.end_temp, tempSymbol)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="monitor-rw-row">
|
||||
<span className="monitor-rw-label">{isEn ? "Runway" : "跑道"}</span>
|
||||
<span className="monitor-rw-temp">{isEn ? "Temp / Dew" : "温度 / 露点"}</span>
|
||||
</div>
|
||||
)}
|
||||
{hasPairRows &&
|
||||
pairRows.map((row) => (
|
||||
<div key={row.label} className="monitor-rw-row">
|
||||
<span className="monitor-rw-label">{row.label}</span>
|
||||
<span className="monitor-rw-temp">
|
||||
{formatTemp(row.temp, tempSymbol)} / {formatTemp(row.dew, tempSymbol)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="scan-empty-state compact">
|
||||
{isEn
|
||||
? `No ${label.sourceLabel} runway observation loaded yet.`
|
||||
: `暂无 ${label.sourceLabel} 跑道观测。`}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function RunwayObservationsPanel() {
|
||||
const { locale } = useI18n();
|
||||
const isEn = locale === "en-US";
|
||||
const { cityDetailsByName } = useCityDetails();
|
||||
const { ensureCityDetail } = useDashboardActions();
|
||||
const loadAll = useCallback(
|
||||
async () => {
|
||||
await Promise.allSettled(
|
||||
RUNWAY_OBSERVATION_CITIES.map((city) =>
|
||||
ensureCityDetail(city.key, true, "panel"),
|
||||
),
|
||||
);
|
||||
},
|
||||
[ensureCityDetail],
|
||||
);
|
||||
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void loadAll();
|
||||
intervalRef.current = setInterval(() => {
|
||||
void Promise.allSettled(
|
||||
RUNWAY_OBSERVATION_CITIES.map((city) =>
|
||||
ensureCityDetail(city.key, true, "panel"),
|
||||
),
|
||||
);
|
||||
}, 60_000);
|
||||
return () => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
};
|
||||
}, [loadAll, ensureCityDetail]);
|
||||
|
||||
const cards = useMemo(
|
||||
() =>
|
||||
RUNWAY_OBSERVATION_CITIES.map((city) => ({
|
||||
...city,
|
||||
detail: cityDetailsByName[city.key],
|
||||
})),
|
||||
[cityDetailsByName],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="monitor-panel runway-observations-panel">
|
||||
<div className="monitor-toolbar">
|
||||
<div className="monitor-title">
|
||||
{isEn ? "Runway Observations" : "跑道观测"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="monitor-grid">
|
||||
{cards.map((city) => (
|
||||
<RunwayCityCard
|
||||
key={city.key}
|
||||
detail={city.detail}
|
||||
isEn={isEn}
|
||||
label={city}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall";
|
||||
import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal";
|
||||
import type { Locale } from "@/lib/i18n";
|
||||
|
||||
export type ScanTerminalContentView = "analysis" | "map" | "monitor" | "runway";
|
||||
export type ScanTerminalContentView = "analysis" | "map";
|
||||
|
||||
type ThemeMode = "dark" | "light";
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
getMonitorRefreshRequest,
|
||||
MONITOR_CITY_DETAIL_DEPTH,
|
||||
} from "@/components/dashboard/monitoring/monitor-refresh-policy";
|
||||
|
||||
export function runTests() {
|
||||
const initial = getMonitorRefreshRequest("initial");
|
||||
assert.equal(
|
||||
initial.force,
|
||||
true,
|
||||
"monitor initial load must force refresh instead of showing 30-minute session cache",
|
||||
);
|
||||
assert.equal(initial.depth, MONITOR_CITY_DETAIL_DEPTH);
|
||||
|
||||
const interval = getMonitorRefreshRequest("interval");
|
||||
assert.equal(interval.force, true);
|
||||
assert.equal(interval.depth, MONITOR_CITY_DETAIL_DEPTH);
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveMonitorTemperature } from "@/components/dashboard/monitoring/monitor-temperature";
|
||||
import type { CityDetail } from "@/lib/dashboard-types";
|
||||
|
||||
function detail(extra: Partial<CityDetail>): CityDetail {
|
||||
return {
|
||||
current: { temp: null },
|
||||
display_name: "Busan",
|
||||
lat: 0,
|
||||
local_date: "2026-05-14",
|
||||
local_time: "15:00",
|
||||
lon: 0,
|
||||
name: "busan",
|
||||
risk: { level: "low" },
|
||||
temp_symbol: "°C",
|
||||
...extra,
|
||||
} as CityDetail;
|
||||
}
|
||||
|
||||
export function runTests() {
|
||||
const busan = detail({
|
||||
airport_current: {
|
||||
obs_time: "15:00",
|
||||
source_label: "METAR",
|
||||
temp: 26,
|
||||
},
|
||||
amos: {
|
||||
runway_obs: {
|
||||
runway_pairs: [["18L", "36R"]],
|
||||
temperatures: [[25.2, 18.1]],
|
||||
},
|
||||
source: "amos",
|
||||
temp: 26,
|
||||
temp_c: 26,
|
||||
temp_source: "metar",
|
||||
},
|
||||
});
|
||||
const busanTemp = resolveMonitorTemperature(busan);
|
||||
assert.equal(busanTemp.value, 25.2);
|
||||
assert.equal(busanTemp.source, "amos_runway");
|
||||
|
||||
const seoul = detail({
|
||||
airport_current: { obs_time: "15:00", temp: 27 },
|
||||
amos: {
|
||||
runway_obs: {
|
||||
runway_pairs: [["15L", "33R"], ["15R", "33L"]],
|
||||
temperatures: [[25.2, 19], [24.8, 18.8]],
|
||||
},
|
||||
source: "amos",
|
||||
temp_source: "metar",
|
||||
},
|
||||
});
|
||||
const seoulTemp = resolveMonitorTemperature(seoul);
|
||||
assert.equal(seoulTemp.value, 25);
|
||||
assert.equal(seoulTemp.source, "amos_runway_median");
|
||||
|
||||
const beijing = detail({
|
||||
display_name: "Beijing",
|
||||
name: "beijing",
|
||||
amos: {
|
||||
runway_obs: {
|
||||
runway_pairs: [["18R", "36L"], ["18L", "36R"]],
|
||||
temperatures: [[20.8, null], [21.0, null]],
|
||||
},
|
||||
source: "amsc_awos",
|
||||
temp_c: 21,
|
||||
temp_source: "runway_max",
|
||||
},
|
||||
});
|
||||
const beijingTemp = resolveMonitorTemperature(beijing);
|
||||
assert.equal(beijingTemp.value, 21);
|
||||
assert.equal(beijingTemp.source, "amsc_awos_runway_max");
|
||||
|
||||
const metarOnly = detail({
|
||||
airport_current: { obs_time: "15:00", temp: 30 },
|
||||
current: { temp: 29 } as CityDetail["current"],
|
||||
});
|
||||
assert.equal(resolveMonitorTemperature(metarOnly).value, 30);
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
export function runTests() {
|
||||
const projectRoot = process.cwd();
|
||||
const shellPartsPath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"scan-terminal",
|
||||
"ScanTerminalShellParts.tsx",
|
||||
);
|
||||
const dashboardPath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"ScanTerminalDashboard.tsx",
|
||||
);
|
||||
const runwayPanelPath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"scan-terminal",
|
||||
"RunwayObservationsPanel.tsx",
|
||||
);
|
||||
const monitorPanelPath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"monitoring",
|
||||
"MonitorPanel.tsx",
|
||||
);
|
||||
|
||||
const shellPartsSource = fs.readFileSync(shellPartsPath, "utf8");
|
||||
const dashboardSource = fs.readFileSync(dashboardPath, "utf8");
|
||||
|
||||
assert(
|
||||
!shellPartsSource.includes('"monitor"') && !shellPartsSource.includes('"runway"'),
|
||||
"scan terminal content views must not include market monitor or runway tabs",
|
||||
);
|
||||
assert(
|
||||
!dashboardSource.includes('setActiveView("monitor")') &&
|
||||
!dashboardSource.includes('setActiveView("runway")') &&
|
||||
!dashboardSource.includes("市场监控") &&
|
||||
!dashboardSource.includes("跑道观测"),
|
||||
"dashboard must not expose market monitor or runway observation tabs",
|
||||
);
|
||||
assert(!fs.existsSync(runwayPanelPath), "dedicated runway observation panel must be removed");
|
||||
assert(!fs.existsSync(monitorPanelPath), "dedicated market monitor panel must be removed");
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
export function runTests() {
|
||||
const projectRoot = process.cwd();
|
||||
const shellPartsPath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"scan-terminal",
|
||||
"ScanTerminalShellParts.tsx",
|
||||
);
|
||||
const dashboardPath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"ScanTerminalDashboard.tsx",
|
||||
);
|
||||
const panelPath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"scan-terminal",
|
||||
"RunwayObservationsPanel.tsx",
|
||||
);
|
||||
const monitorPath = path.join(
|
||||
projectRoot,
|
||||
"components",
|
||||
"dashboard",
|
||||
"monitoring",
|
||||
"MonitorPanel.tsx",
|
||||
);
|
||||
|
||||
const shellPartsSource = fs.readFileSync(shellPartsPath, "utf8");
|
||||
const dashboardSource = fs.readFileSync(dashboardPath, "utf8");
|
||||
assert(
|
||||
shellPartsSource.includes('"runway"'),
|
||||
"scan terminal content view must include a runway tab state",
|
||||
);
|
||||
assert(
|
||||
dashboardSource.includes("跑道观测") && dashboardSource.includes('setActiveView("runway")'),
|
||||
"dashboard tabs must expose 跑道观测 next to 市场监控",
|
||||
);
|
||||
assert(
|
||||
fs.existsSync(panelPath),
|
||||
"RunwayObservationsPanel.tsx must render the dedicated runway tab body",
|
||||
);
|
||||
const panelSource = fs.readFileSync(panelPath, "utf8");
|
||||
assert(
|
||||
panelSource.includes("AMSC AWOS") &&
|
||||
panelSource.includes("TDZ") &&
|
||||
panelSource.includes("MID") &&
|
||||
panelSource.includes("END"),
|
||||
"runway tab must identify AMSC AWOS and show TDZ/MID/END point temperatures",
|
||||
);
|
||||
assert(
|
||||
panelSource.includes('key: "qingdao"') &&
|
||||
panelSource.includes("青岛") &&
|
||||
panelSource.includes("ZSQD"),
|
||||
"runway tab must include Qingdao / ZSQD AMSC AWOS runway observations",
|
||||
);
|
||||
assert(
|
||||
panelSource.includes("RKSI") &&
|
||||
panelSource.includes("RKPK") &&
|
||||
panelSource.includes("runway_pairs") &&
|
||||
panelSource.includes("runway_temps"),
|
||||
"runway tab must also own Seoul/Busan AMOS runway-pair observations",
|
||||
);
|
||||
const monitorSource = fs.readFileSync(monitorPath, "utf8");
|
||||
assert(
|
||||
monitorSource.includes("ignoreRunway: KOREA_RUNWAY_MONITOR_KEYS.has(key)") &&
|
||||
monitorSource.includes("showRunwayRows = !KOREA_RUNWAY_MONITOR_KEYS.has(key)"),
|
||||
"market monitor must not render Seoul/Busan runway data after it moves to the runway tab",
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user