feat: implement real-time observation patch normalization and live temperature threshold visualization logic

This commit is contained in:
2569718930@qq.com
2026-05-27 10:17:33 +08:00
parent e6a673e27d
commit 820dabfbf3
23 changed files with 709 additions and 29 deletions
@@ -152,6 +152,19 @@ export function runTests() {
!resyncBlock.includes("setIsHourlyLoading(true)"),
"SSE replay resync should refresh full detail in the background without showing the loading overlay",
);
assert(
chart.includes("visibilitychange") &&
chart.includes('document.visibilityState !== "visible"') &&
chart.includes("refreshForegroundFullDetail"),
"temperature chart must immediately refresh visible charts when the browser tab returns to the foreground",
);
const foregroundRefreshBlock = chart.match(/const refreshForegroundFullDetail = \(\) => \{[\s\S]*?\n \};/)?.[0] || "";
assert(
foregroundRefreshBlock.includes("ignoreCache: true") &&
foregroundRefreshBlock.includes("fetchHourlyForecastForCity") &&
!foregroundRefreshBlock.includes("setIsHourlyLoading(true)"),
"foreground resume refresh should update full detail immediately in the background without showing the loading overlay",
);
assert(chart.includes("viewMode"), "temperature chart must expose a view mode for DEB-peak auto view versus full-day view");
assert(chart.includes('useState<"auto" | "full">("full")'), "temperature chart must default every city panel to the all-day view");
assert(
@@ -176,6 +189,19 @@ export function runTests() {
chart.includes("prefersHighFrequencyRunwayResolution") && chart.includes('return "1m";'),
"runway charts must request 1-minute detail resolution so historical runway lines match live SSE patch cadence",
);
assert(
chart.includes("PROBABILITY_REFRESH_AFTER_PATCH_MS") &&
chart.includes("lastProbabilityRefreshAtRef") &&
chart.includes("refreshProbabilityOverlayAfterPatch"),
"temperature chart must trigger a throttled background probability refresh after live observation patches",
);
const patchEffectBlock = chart.match(/useEffect\(\(\) => \{\s*if \(!latestPatch[\s\S]*?\}, \[latestPatch, row, city, targetResolution, compact, isActive, isMaximized\]\);/)?.[0] || "";
assert(
patchEffectBlock.includes("refreshProbabilityOverlayAfterPatch") &&
patchEffectBlock.includes("ignoreCache: true") &&
!patchEffectBlock.includes("setIsHourlyLoading(true)"),
"live patch probability refresh must recompute legacy Gaussian in the background without showing a loading overlay",
);
assert(!chartCanvas.includes("ResponsiveContainer"), "temperature chart canvas must not mount Recharts through ResponsiveContainer at 0x0");
assert(chartCanvas.includes("ResizeObserver"), "temperature chart canvas must measure its host with ResizeObserver");
assert(
@@ -10,7 +10,7 @@ import {
__mergePatchIntoHourlyForTest,
} from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart";
function assert(condition: unknown, message: string) {
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
@@ -802,6 +802,52 @@ export function runTests() {
"latest airport/METAR report should be appended to the live chart series even when history stops earlier",
);
const torontoCanonicalPatchHourly = __mergePatchIntoHourlyForTest(
{
localTime: "19:15",
localDate: "2026-05-27",
times: ["10:00", "13:00", "16:00", "19:00"],
temps: [23, 26, 27, 26],
airportPrimaryTodayObs: [],
} as any,
{
type: "city_observation_patch.v1",
city: "toronto",
revision: 13,
changes: {
temp: 26,
source: "metar",
observed_at_utc: "2026-05-27T23:16:00Z",
observed_at_local: "2026-05-27T19:16:00-04:00",
city_local_date: "2026-05-27",
city_timezone: "America/Toronto",
},
} as any,
);
assert(
torontoCanonicalPatchHourly,
"v1 canonical patch should merge into hourly forecast",
);
const torontoCanonicalPatchChart = __buildTemperatureChartDataForTest(
{
city: "toronto",
local_date: "2026-05-27",
local_time: "19:16",
tz_offset_seconds: -4 * 60 * 60,
temp_symbol: "°C",
} as any,
torontoCanonicalPatchHourly as any,
"1D",
);
assert(
torontoCanonicalPatchHourly.localDate === "2026-05-27",
"v1 canonical patch should update hourly localDate from city_local_date",
);
assert(
torontoCanonicalPatchChart.data.some((point) => point.label === "19:16:00" && point.madis === 26),
"v1 canonical patch observed_at_utc should render at the city-local chart time",
);
const newYorkMinuteStream = __buildTemperatureChartDataForTest(
{
city: "new york",
@@ -1039,4 +1085,45 @@ export function runTests() {
assert(bandPoints.length >= 2, "runway_band tuples should be binned into data slots");
const firstBand = bandPoints[0].runway_band;
assert(Array.isArray(firstBand) && firstBand[0] === 24.0 && firstBand[1] === 26.0, "runway_band tuple values should match input limits");
// ── Legacy Gaussian probability overlay test ──
const gaussianOverlayChart = __buildTemperatureChartDataForTest(
{
city: "toronto",
local_date: "2026-05-27",
local_time: "14:00",
tz_offset_seconds: -4 * 60 * 60,
temp_symbol: "°C",
} as any,
{
localDate: "2026-05-27",
localTime: "14:00",
times: ["10:00", "14:00", "18:00"],
temps: [24, 27, 23],
probabilities: {
mu: 27.4,
engine: "legacy",
distribution_all: [
{ value: 26, probability: 0.18, range: "[25.5~26.5)" },
{ value: 27, probability: 0.42, range: "[26.5~27.5)" },
{ value: 28, probability: 0.31, range: "[27.5~28.5)" },
],
},
} as any,
"1D",
) as any;
const gaussianOverlay = gaussianOverlayChart.probabilityOverlay;
assert(gaussianOverlay, "legacy Gaussian probabilities should be exposed as a chart overlay");
assert(gaussianOverlay.muLine?.value === 27.4, "legacy Gaussian μ should become a reference line");
assert(
gaussianOverlay.bands.some(
(band: any) => band.value === 27 && band.lower === 26.5 && band.upper === 27.5 && band.probability === 0.42,
),
"legacy Gaussian buckets should become horizontal probability temperature bands",
);
assert(
!gaussianOverlayChart.series.some((series: any) => String(series.key || "").includes("probability")),
"legacy Gaussian probability distribution should not be rendered as a time-series line",
);
}