feat: add scan terminal dashboard components, monitoring panels, and associated utility hooks

This commit is contained in:
2569718930@qq.com
2026-05-15 12:52:39 +08:00
parent 1a284c9990
commit e7fc3c97cb
13 changed files with 316 additions and 68 deletions
@@ -0,0 +1,30 @@
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 clientPath = path.join(
projectRoot,
"components",
"dashboard",
"scan-terminal",
"scan-terminal-client.ts",
);
const source = fs.readFileSync(clientPath, "utf8");
const concurrencyMatch = source.match(/AI_CITY_READ_MAX_CONCURRENT_STREAMS\s*=\s*(\d+)/);
const concurrency = Number(concurrencyMatch?.[1]);
assert(
Number.isFinite(concurrency) && concurrency >= 4,
"AI airport-read streams should allow at least 4 concurrent cards to avoid slow decision-card evidence queues",
);
assert(
source.includes("queuedAiCityReadTasks.unshift(run)") &&
source.includes("queuedAiCityReadTasks.pop()"),
"newer AI airport-read requests should be prioritized instead of waiting behind older pinned cards",
);
}
@@ -0,0 +1,27 @@
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 workspacePath = path.join(
projectRoot,
"components",
"dashboard",
"scan-terminal",
"use-ai-pinned-city-workspace.ts",
);
const source = fs.readFileSync(workspacePath, "utf8");
assert(
!source.includes("waitForDeepAnalysisQueue"),
"decision-card deep analysis hydration must not add an artificial queue delay",
);
assert(
/store\.ensureCityDetail\(\s*nextCity,\s*false,\s*"full",?\s*\)/.test(source),
"automatic deep analysis hydration should use cache-friendly full detail requests",
);
}
@@ -0,0 +1,23 @@
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 dashboardPath = path.join(
projectRoot,
"components",
"dashboard",
"ScanTerminalDashboard.tsx",
);
const source = fs.readFileSync(dashboardPath, "utf8");
assert(
source.includes("isMobileViewport") &&
source.includes("showAnnouncement && !isMobileViewport"),
"scan upgrade announcement must not render on mobile viewports",
);
}
@@ -0,0 +1,24 @@
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 hookPath = path.join(projectRoot, "hooks", "useLeafletMap.ts");
const source = fs.readFileSync(hookPath, "utf8");
assert(
source.includes("bindMarkerTouchSelect") &&
source.includes('"touchend"') &&
source.includes('"pointerup"'),
"Leaflet city markers must bind touch/pointer selection events for mobile taps",
);
assert(
source.includes("L.DomEvent.stopPropagation") &&
source.includes("L.DomEvent.preventDefault"),
"mobile marker tap handling must stop map click propagation and prevent browser touch defaults",
);
}
@@ -27,6 +27,13 @@ export function runTests() {
"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");
@@ -50,4 +57,23 @@ export function runTests() {
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",
);
}