feat: implement interactive temperature chart dashboard with runway trend visualization and multi-source data collection support

This commit is contained in:
2569718930@qq.com
2026-05-27 01:16:49 +08:00
parent c4119c1aab
commit 2670e2f8ee
13 changed files with 308 additions and 52 deletions
@@ -6,12 +6,23 @@ import {
} from "@/lib/refresh-policy";
import { scanTerminalQueryPolicy } from "@/components/dashboard/scan-terminal/scan-terminal-client";
import { __shouldPollLiveChartForTest } from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart";
import {
MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS,
__resetHourlyDetailRequestQueueForTest,
__runQueuedHourlyDetailRequestForTest,
} from "@/components/dashboard/scan-terminal/temperature-chart-logic";
function assert(condition: unknown, message: string) {
if (!condition) throw new Error(message);
}
export function runTests() {
async function flushMicrotasks() {
for (let i = 0; i < 8; i += 1) {
await Promise.resolve();
}
}
export async function runTests() {
assert(DASHBOARD_REFRESH_POLICY_MS.observation === 60_000, "observation layer should refresh every 60 seconds");
assert(DASHBOARD_REFRESH_POLICY_MS.scanRows === 5 * 60_000, "region/city rows should refresh every 5 minutes");
assert(DASHBOARD_REFRESH_POLICY_MS.marketOverview === 10 * 60_000, "market overview should refresh every 10 minutes");
@@ -68,4 +79,55 @@ export function runTests() {
chartSource.includes("setHourly(seedHourlyForecastFromRow(row))"),
"terminal charts should render from row data immediately and dedupe concurrent city detail requests",
);
__resetHourlyDetailRequestQueueForTest();
let activeRequests = 0;
let maxActiveRequests = 0;
let startedRequests = 0;
const releases: Array<(() => void) | undefined> = [];
const requestCount = MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS + 3;
const requests = Array.from({ length: requestCount }, (_, index) =>
__runQueuedHourlyDetailRequestForTest(
() =>
new Promise<number>((resolve) => {
startedRequests += 1;
activeRequests += 1;
maxActiveRequests = Math.max(maxActiveRequests, activeRequests);
releases[index] = () => {
activeRequests -= 1;
resolve(index);
};
}),
),
);
await flushMicrotasks();
assert(
startedRequests === MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS,
"city detail queue should start only the configured number of concurrent requests",
);
assert(
maxActiveRequests === MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS,
"city detail queue must cap simultaneous full-detail requests",
);
releases[0]?.();
await flushMicrotasks();
assert(
startedRequests === MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS + 1,
"city detail queue should start the next pending request when one active request finishes",
);
for (let index = 1; index < requestCount; index += 1) {
await flushMicrotasks();
assert(Boolean(releases[index]), `city detail queue should eventually start queued request #${index}`);
releases[index]?.();
}
const results = await Promise.all(requests);
assert(
results.length === requestCount && results.every((value, index) => value === index),
"city detail queue should resolve every queued request in order",
);
__resetHourlyDetailRequestQueueForTest();
}
@@ -91,12 +91,16 @@ export function runTests() {
const chartStatsPath = path.join(process.cwd(), "components", "dashboard", "scan-terminal", "TemperatureStatsBars.tsx");
const chartRunwayPath = path.join(process.cwd(), "components", "dashboard", "scan-terminal", "TemperatureRunwayDetails.tsx");
const chartTooltipPath = path.join(process.cwd(), "components", "dashboard", "scan-terminal", "TemperatureTooltipContent.tsx");
const chartSummaryPath = path.join(process.cwd(), "components", "dashboard", "scan-terminal", "ModelCurvesSummary.tsx");
assert(fs.existsSync(chartCanvasPath), "temperature chart Recharts canvas must live in TemperatureChartCanvas.tsx");
assert(fs.existsSync(chartStatsPath), "temperature chart stat bars must live in TemperatureStatsBars.tsx");
assert(fs.existsSync(chartRunwayPath), "temperature chart runway detail panel must live in TemperatureRunwayDetails.tsx");
assert(fs.existsSync(chartTooltipPath), "temperature chart tooltip must live in TemperatureTooltipContent.tsx");
assert(fs.existsSync(chartSummaryPath), "temperature chart model summary must live in ModelCurvesSummary.tsx");
const chartCanvas = fs.readFileSync(chartCanvasPath, "utf8");
const chartTooltip = fs.readFileSync(chartTooltipPath, "utf8");
const chartRunway = fs.readFileSync(chartRunwayPath, "utf8");
const chartSummary = fs.readFileSync(chartSummaryPath, "utf8");
const chartLogicPath = path.join(process.cwd(), "components", "dashboard", "scan-terminal", "temperature-chart-logic.ts");
assert(fs.existsSync(chartLogicPath), "temperature chart pure data logic must live in temperature-chart-logic.ts");
const chartLogic = fs.readFileSync(chartLogicPath, "utf8");
@@ -112,6 +116,18 @@ export function runTests() {
assert(chart.includes("TemperatureChartCanvas"), "temperature chart shell must compose the extracted chart canvas");
assert(chart.includes("TemperatureStatsBars"), "temperature chart shell must compose the extracted stat bars");
assert(chart.includes("TemperatureRunwayDetails"), "temperature chart shell must compose the extracted runway panel");
assert(chart.includes("tempSymbol={row?.temp_symbol || \"°C\"}"), "temperature chart shell must pass the city temperature unit into stat bars");
assert(chart.includes("<TemperatureRunwayDetails") && chart.includes("tempSymbol={row?.temp_symbol || \"°C\"}"), "temperature chart shell must pass the city unit into runway details");
assert(chart.includes("<ModelCurvesSummary") && chart.includes("tempSymbol={row?.temp_symbol || \"°C\"}"), "temperature chart shell must pass the city unit into model summaries");
const chartStats = fs.readFileSync(chartStatsPath, "utf8");
assert(chartStats.includes("tempSymbol"), "temperature stat bars must accept the city temperature unit");
assert(chartStats.includes("temp(displayRunwayTemp, tempSymbol)"), "temperature stat bars must render live observations with the city unit");
assert(chartRunway.includes("tempSymbol") && !chartRunway.includes("}°C`"), "runway detail rows must render values with the city unit");
assert(chartSummary.includes("tempSymbol") && chartSummary.includes("temp(stats.latest, tempSymbol)"), "model curve summaries must render values with the city unit");
assert(chartCanvas.includes("const tempSymbol = row?.temp_symbol || \"°C\""), "temperature chart canvas must derive the city unit from the row");
assert(chartCanvas.includes("tempSymbol={tempSymbol}"), "temperature chart canvas must pass the city unit into tooltips");
assert(chartCanvas.includes("tickFormatter={(v) => `${Number(v).toFixed(0)}${tempSymbol}`}"), "temperature y-axis ticks must include °C/°F instead of a bare degree mark");
assert(chartTooltip.includes("tempSymbol"), "temperature tooltip must accept the city temperature unit");
assert(!chart.includes("from \"recharts\""), "LiveTemperatureThresholdChart.tsx must not import Recharts directly");
assert(!chart.includes("function TemperatureTooltipContent"), "LiveTemperatureThresholdChart.tsx must not define tooltip content inline");
assert(chartCanvas.includes("TemperatureTooltipContent"), "temperature chart canvas must use a custom tooltip content component");