feat: implement real-time observation patch normalization and live temperature threshold visualization logic
This commit is contained in:
@@ -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(
|
||||
|
||||
+88
-1
@@ -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",
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user