Keep model summary data during refresh gaps
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import clsx from "clsx";
|
||||
import { Search, Table2 } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||
import {
|
||||
MODEL_SUMMARY_MODEL_COLUMNS,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
filterModelSummaryRows,
|
||||
formatModelSummaryLocalTime,
|
||||
formatModelSummaryTemp,
|
||||
hasModelSummaryForecastData,
|
||||
type ModelSummaryRow,
|
||||
} from "@/lib/model-summary";
|
||||
|
||||
@@ -132,6 +133,43 @@ function ModelSummaryRowView({
|
||||
);
|
||||
}
|
||||
|
||||
function mergeWithLastGoodSummaryRows(
|
||||
incomingRows: ModelSummaryRow[],
|
||||
lastGoodRows: ModelSummaryRow[],
|
||||
) {
|
||||
if (!lastGoodRows.length) return incomingRows;
|
||||
if (!incomingRows.length) return lastGoodRows;
|
||||
|
||||
const incomingCityKeys = new Set<string>();
|
||||
const lastGoodByCity = new Map(lastGoodRows.map((row) => [row.cityKey, row]));
|
||||
const mergedRows = incomingRows.map((incomingRow) => {
|
||||
incomingCityKeys.add(incomingRow.cityKey);
|
||||
const lastGoodRow = lastGoodByCity.get(incomingRow.cityKey);
|
||||
if (!lastGoodRow) return incomingRow;
|
||||
|
||||
return {
|
||||
...lastGoodRow,
|
||||
cityName: incomingRow.cityName,
|
||||
regionLabel: incomingRow.regionLabel,
|
||||
regionLabelZh: incomingRow.regionLabelZh,
|
||||
regionSort: incomingRow.regionSort,
|
||||
tempSymbol: incomingRow.tempSymbol || lastGoodRow.tempSymbol,
|
||||
localTime: incomingRow.localTime || lastGoodRow.localTime,
|
||||
timezoneOffsetSeconds:
|
||||
incomingRow.timezoneOffsetSeconds ?? lastGoodRow.timezoneOffsetSeconds,
|
||||
searchText: incomingRow.searchText || lastGoodRow.searchText,
|
||||
};
|
||||
});
|
||||
|
||||
return [
|
||||
...mergedRows,
|
||||
...lastGoodRows.filter((row) => !incomingCityKeys.has(row.cityKey)),
|
||||
].sort((a, b) => {
|
||||
if (a.regionSort !== b.regionSort) return a.regionSort - b.regionSort;
|
||||
return a.cityName.localeCompare(b.cityName, "en", { sensitivity: "base" });
|
||||
});
|
||||
}
|
||||
|
||||
export function ModelSummaryDashboard({
|
||||
rows,
|
||||
isEn,
|
||||
@@ -141,6 +179,7 @@ export function ModelSummaryDashboard({
|
||||
const [debOnly, setDebOnly] = useState(false);
|
||||
const [wideSpreadOnly, setWideSpreadOnly] = useState(false);
|
||||
const [nowMs, setNowMs] = useState<number | null>(null);
|
||||
const lastGoodSummaryRowsRef = useRef<ModelSummaryRow[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const syncClock = () => setNowMs(Date.now());
|
||||
@@ -149,7 +188,22 @@ export function ModelSummaryDashboard({
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const summaryRows = useMemo(() => buildModelSummaryRows(rows, isEn), [rows, isEn]);
|
||||
const incomingSummaryRows = useMemo(() => buildModelSummaryRows(rows, isEn), [rows, isEn]);
|
||||
const incomingHasForecastData = hasModelSummaryForecastData(incomingSummaryRows);
|
||||
const summaryRows = useMemo(
|
||||
() =>
|
||||
incomingHasForecastData
|
||||
? incomingSummaryRows
|
||||
: mergeWithLastGoodSummaryRows(incomingSummaryRows, lastGoodSummaryRowsRef.current),
|
||||
[incomingSummaryRows, incomingHasForecastData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (incomingHasForecastData) {
|
||||
lastGoodSummaryRowsRef.current = incomingSummaryRows;
|
||||
}
|
||||
}, [incomingSummaryRows, incomingHasForecastData]);
|
||||
|
||||
const visibleRows = useMemo(
|
||||
() =>
|
||||
filterModelSummaryRows(summaryRows, {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
filterModelSummaryRows,
|
||||
formatModelSummaryLocalTime,
|
||||
formatModelSummaryTemp,
|
||||
hasModelSummaryForecastData,
|
||||
} from "@/lib/model-summary";
|
||||
|
||||
function assert(condition: unknown, message: string) {
|
||||
@@ -113,6 +114,24 @@ export function runTests() {
|
||||
assert(parisRow.modelSpread === 2.5, "model spread should use available model min/max only");
|
||||
assert(formatModelSummaryTemp(null, "°C") === "—", "missing model temperatures should render as an em dash");
|
||||
assert(formatModelSummaryTemp(32.16, "°C") === "32.2°C", "model temperatures should render to one decimal");
|
||||
assert(hasModelSummaryForecastData(summaryRows), "model summary should recognize populated forecast rows");
|
||||
assert(
|
||||
!hasModelSummaryForecastData(
|
||||
buildModelSummaryRows([
|
||||
{
|
||||
id: "fallback:beijing",
|
||||
city: "beijing",
|
||||
city_display_name: "Beijing",
|
||||
trading_region_label: "East Asia",
|
||||
trading_region_label_zh: "东亚",
|
||||
trading_region_sort: 1,
|
||||
local_time: "21:11",
|
||||
tz_offset_seconds: 28800,
|
||||
},
|
||||
] as any, false),
|
||||
),
|
||||
"model summary should recognize fallback-only rows without DEB or model forecasts",
|
||||
);
|
||||
|
||||
const searched = filterModelSummaryRows(summaryRows, {
|
||||
debOnly: true,
|
||||
@@ -155,6 +174,8 @@ export function runTests() {
|
||||
);
|
||||
assert(
|
||||
modelSummarySource.includes("MODEL_SUMMARY_MODEL_COLUMNS") &&
|
||||
modelSummarySource.includes("lastGoodSummaryRowsRef") &&
|
||||
modelSummarySource.includes("hasModelSummaryForecastData") &&
|
||||
modelSummarySource.includes("Local Time") &&
|
||||
modelSummarySource.includes("当地时间") &&
|
||||
!modelSummarySource.includes("Current High") &&
|
||||
|
||||
@@ -196,3 +196,10 @@ export function filterModelSummaryRows(
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function hasModelSummaryForecastData(rows: ModelSummaryRow[]) {
|
||||
return rows.some((row) => {
|
||||
if (row.debPrediction != null) return true;
|
||||
return MODEL_SUMMARY_MODEL_COLUMNS.some((column) => row.models[column.key] != null);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user