Preserve terminal chart detail during refresh
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
||||
readSessionCache,
|
||||
selectCompactSecondaryTemp,
|
||||
selectDisplayRunwayTemp,
|
||||
selectInitialHourlyForRowChange,
|
||||
seedHourlyForecastFromRow,
|
||||
shouldPollLiveChart,
|
||||
validNumber,
|
||||
@@ -528,6 +529,30 @@ function getInitialDetailLoadDelayMs({
|
||||
return DEFERRED_DETAIL_LOAD_DELAY_MS + deferredWave * DEFERRED_DETAIL_LOAD_WAVE_STEP_MS;
|
||||
}
|
||||
|
||||
function readCachedHourlyForInitialRow(
|
||||
city: string,
|
||||
preferredResolution: string,
|
||||
): HourlyForecast {
|
||||
const cityKey = String(city || "").toLowerCase().trim();
|
||||
if (!cityKey) return null;
|
||||
const resolutions = [
|
||||
preferredResolution,
|
||||
preferredResolution === "1m" ? "10m" : "1m",
|
||||
].filter((value, index, list) => value && list.indexOf(value) === index);
|
||||
|
||||
for (const resolution of resolutions) {
|
||||
const cacheKey = `${cityKey}:${resolution}`;
|
||||
const memoryEntry = _hourlyCache.get(cacheKey);
|
||||
if (memoryEntry?.data) return memoryEntry.data;
|
||||
const sessionEntry = readSessionCache(cacheKey, { allowStale: true });
|
||||
if (sessionEntry?.data) {
|
||||
_hourlyCache.set(cacheKey, sessionEntry);
|
||||
return sessionEntry.data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Main component ─────────────────────────────────────────────────────
|
||||
|
||||
export function LiveTemperatureThresholdChart({
|
||||
@@ -575,6 +600,7 @@ export function LiveTemperatureThresholdChart({
|
||||
createChartFreshnessState(),
|
||||
);
|
||||
const hasLoadedHourlyDetailRef = useRef(false);
|
||||
const hourlyCityRef = useRef<string>("");
|
||||
const chartVisibilityRef = useRef<HTMLDivElement | null>(null);
|
||||
const lastPatchAtRef = useRef<number>(Date.now());
|
||||
const lastAppliedPatchRevisionRef = useRef<number>(0);
|
||||
@@ -624,12 +650,23 @@ export function LiveTemperatureThresholdChart({
|
||||
|
||||
useEffect(() => {
|
||||
const now = Date.now();
|
||||
const previousCity = hourlyCityRef.current;
|
||||
hourlyCityRef.current = city;
|
||||
const nextTargetResolution = prefersHighFrequencyRunwayResolution(row, null) ? "1m" : "10m";
|
||||
const cachedHourly = readCachedHourlyForInitialRow(city, nextTargetResolution);
|
||||
setUserToggledKeys({});
|
||||
setZoomRange(null);
|
||||
setViewMode("full");
|
||||
setShowRunwayDetails(true);
|
||||
setTargetResolution(prefersHighFrequencyRunwayResolution(row, null) ? "1m" : "10m");
|
||||
setHourly(seedHourlyForecastFromRow(row));
|
||||
setTargetResolution(nextTargetResolution);
|
||||
setHourly((prev) =>
|
||||
selectInitialHourlyForRowChange({
|
||||
cachedHourly,
|
||||
previousCity,
|
||||
previousHourly: prev,
|
||||
row,
|
||||
}),
|
||||
);
|
||||
setLiveTemp(null);
|
||||
setIsHourlyLoading(Boolean(city) && detailLoadDelayMs === 0);
|
||||
setDetailError(null);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
mergeHourlyWithLiveObservations,
|
||||
mergePatchIntoHourly,
|
||||
mergeRowObservationIntoHourly,
|
||||
selectInitialHourlyForRowChange,
|
||||
seedHourlyForecastFromRow,
|
||||
} from "@/components/dashboard/scan-terminal/temperature-chart-logic";
|
||||
|
||||
@@ -474,4 +475,107 @@ export function runTests() {
|
||||
chengduMerged?.airportCurrent?.temp === 28,
|
||||
"stale previous-day scan rows must not replace current-date detail airport conditions",
|
||||
);
|
||||
|
||||
const shenzhenRow = {
|
||||
city: "shenzhen",
|
||||
local_date: "2026-06-10",
|
||||
local_time: "12:03",
|
||||
current_temp: 31.2,
|
||||
current_max_so_far: 31.2,
|
||||
temp_symbol: "°C",
|
||||
tz_offset_seconds: 8 * 3600,
|
||||
} as any;
|
||||
const shenzhenFullDetail = {
|
||||
...seedHourlyForecastFromRow(shenzhenRow),
|
||||
localDate: "2026-06-10",
|
||||
localTime: "12:00",
|
||||
times: ["10:00", "11:00", "12:00"],
|
||||
temps: [28, 30, 31],
|
||||
modelTimes: ["10:00", "11:00", "12:00"],
|
||||
modelCurves: {
|
||||
ECMWF: [28.2, 30.1, 31.1],
|
||||
GFS: [27.9, 29.8, 30.9],
|
||||
},
|
||||
debPrediction: 33,
|
||||
debHourlyPath: {
|
||||
source: "deb_hourly_consensus",
|
||||
times: ["10:00", "11:00", "12:00"],
|
||||
temps: [29, 31, 33],
|
||||
},
|
||||
multiModelDaily: {
|
||||
"2026-06-10": {
|
||||
deb: { prediction: 33 },
|
||||
models: { ECMWF: 33.2, GFS: 32.7 },
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const shenzhenUpdatedRow = {
|
||||
...shenzhenRow,
|
||||
local_time: "12:07",
|
||||
current_temp: 31.6,
|
||||
current_max_so_far: 31.6,
|
||||
} as any;
|
||||
const shenzhenInitialAfterReturn = selectInitialHourlyForRowChange({
|
||||
previousCity: "shenzhen",
|
||||
previousHourly: shenzhenFullDetail,
|
||||
row: shenzhenUpdatedRow,
|
||||
});
|
||||
assert(
|
||||
shenzhenInitialAfterReturn?.modelCurves?.ECMWF?.length === 3,
|
||||
"returning to terminal must keep existing multi-model curves while the detail refresh is pending",
|
||||
);
|
||||
assert(
|
||||
shenzhenInitialAfterReturn?.debHourlyPath?.temps?.includes(33),
|
||||
"returning to terminal must keep the existing DEB hourly path while the detail refresh is pending",
|
||||
);
|
||||
assert(
|
||||
shenzhenInitialAfterReturn?.airportCurrent?.temp === 31.6,
|
||||
"returning to terminal should still merge the newest scan-row observation into the preserved detail",
|
||||
);
|
||||
|
||||
const hongKongCachedDetail = {
|
||||
...seedHourlyForecastFromRow({ ...shenzhenRow, city: "hongkong" } as any),
|
||||
localDate: "2026-06-10",
|
||||
times: ["10:00", "11:00"],
|
||||
temps: [29.5, 30.2],
|
||||
modelCurves: { ECMWF: [29.8, 30.5] },
|
||||
debPrediction: 31,
|
||||
debHourlyPath: {
|
||||
source: "deb_hourly_consensus",
|
||||
times: ["10:00", "11:00"],
|
||||
temps: [30, 31],
|
||||
},
|
||||
} as any;
|
||||
const hongKongRow = {
|
||||
...shenzhenRow,
|
||||
city: "hongkong",
|
||||
local_time: "12:10",
|
||||
current_temp: 30.7,
|
||||
} as any;
|
||||
const hongKongInitialAfterSelect = selectInitialHourlyForRowChange({
|
||||
cachedHourly: hongKongCachedDetail,
|
||||
previousCity: "shenzhen",
|
||||
previousHourly: shenzhenFullDetail,
|
||||
row: hongKongRow,
|
||||
});
|
||||
assert(
|
||||
hongKongInitialAfterSelect?.modelCurves?.ECMWF?.length === 2 &&
|
||||
hongKongInitialAfterSelect?.debHourlyPath?.temps?.includes(31),
|
||||
"selecting a city with cached detail must render cached multi-model and DEB immediately instead of falling back to an empty row seed",
|
||||
);
|
||||
assert(
|
||||
!hongKongInitialAfterSelect?.modelCurves?.GFS,
|
||||
"selecting a different city must not leak the previous city's model curves",
|
||||
);
|
||||
|
||||
const uncachedNewCityInitial = selectInitialHourlyForRowChange({
|
||||
previousCity: "shenzhen",
|
||||
previousHourly: shenzhenFullDetail,
|
||||
row: { ...shenzhenRow, city: "tokyo" } as any,
|
||||
});
|
||||
assert(
|
||||
!uncachedNewCityInitial?.modelCurves?.ECMWF &&
|
||||
!uncachedNewCityInitial?.debHourlyPath,
|
||||
"selecting an uncached different city must not show another city's model and DEB curves",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1406,6 +1406,32 @@ function mergeRowObservationIntoHourly(
|
||||
return mergeHourlyWithLiveObservations(prev, seeded, row);
|
||||
}
|
||||
|
||||
function selectInitialHourlyForRowChange({
|
||||
cachedHourly,
|
||||
previousCity,
|
||||
previousHourly,
|
||||
row,
|
||||
}: {
|
||||
cachedHourly?: HourlyForecast;
|
||||
previousCity?: string | null;
|
||||
previousHourly?: HourlyForecast;
|
||||
row: ScanOpportunityRow | null;
|
||||
}): HourlyForecast {
|
||||
const seeded = seedHourlyForecastFromRow(row);
|
||||
const nextCity = normalizeCityKey(row?.city);
|
||||
if (!nextCity) return seeded;
|
||||
|
||||
if (cachedHourly) {
|
||||
return mergeHourlyWithLiveObservations(cachedHourly, seeded, row);
|
||||
}
|
||||
|
||||
if (previousHourly && normalizeCityKey(previousCity) === nextCity) {
|
||||
return mergeHourlyWithLiveObservations(previousHourly, seeded, row);
|
||||
}
|
||||
|
||||
return seeded;
|
||||
}
|
||||
|
||||
type HourlyForecastFetchOptions = {
|
||||
bypassLocalCache?: boolean;
|
||||
ignoreCache?: boolean;
|
||||
@@ -2980,6 +3006,7 @@ export {
|
||||
readSessionCache,
|
||||
selectCompactSecondaryTemp,
|
||||
selectDisplayRunwayTemp,
|
||||
selectInitialHourlyForRowChange,
|
||||
seedHourlyForecastFromRow,
|
||||
seriesStats,
|
||||
shouldPollLiveChart,
|
||||
|
||||
Reference in New Issue
Block a user