Fix model summary live local time
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
|
||||
import clsx from "clsx";
|
||||
import { Search, Table2 } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import type { ScanOpportunityRow } from "@/lib/dashboard-types";
|
||||
import {
|
||||
MODEL_SUMMARY_MODEL_COLUMNS,
|
||||
buildModelSummaryRows,
|
||||
filterModelSummaryRows,
|
||||
formatModelSummaryLocalTime,
|
||||
formatModelSummaryTemp,
|
||||
type ModelSummaryRow,
|
||||
} from "@/lib/model-summary";
|
||||
@@ -94,8 +95,10 @@ function FilterToggle({
|
||||
|
||||
function ModelSummaryRowView({
|
||||
row,
|
||||
nowMs,
|
||||
}: {
|
||||
row: ModelSummaryRow;
|
||||
nowMs: number | null;
|
||||
}) {
|
||||
return (
|
||||
<tr className="group border-b border-slate-100 hover:bg-blue-50/40">
|
||||
@@ -109,7 +112,7 @@ function ModelSummaryRowView({
|
||||
<span className="block truncate">{row.regionLabel}</span>
|
||||
</td>
|
||||
<td className="min-w-[96px] px-3 py-2 text-right font-mono text-[11px] font-bold text-slate-700">
|
||||
{row.localTime || "—"}
|
||||
{formatModelSummaryLocalTime(row, nowMs)}
|
||||
</td>
|
||||
<td className="min-w-[90px] px-3 py-2 text-right">
|
||||
<TemperatureCell value={row.debPrediction} symbol={row.tempSymbol} emphasis="deb" />
|
||||
@@ -137,6 +140,14 @@ export function ModelSummaryDashboard({
|
||||
const [query, setQuery] = useState("");
|
||||
const [debOnly, setDebOnly] = useState(false);
|
||||
const [wideSpreadOnly, setWideSpreadOnly] = useState(false);
|
||||
const [nowMs, setNowMs] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const syncClock = () => setNowMs(Date.now());
|
||||
syncClock();
|
||||
const timer = window.setInterval(syncClock, 30_000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
const summaryRows = useMemo(() => buildModelSummaryRows(rows, isEn), [rows, isEn]);
|
||||
const visibleRows = useMemo(
|
||||
@@ -216,7 +227,7 @@ export function ModelSummaryDashboard({
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 bg-white">
|
||||
{visibleRows.map((row) => (
|
||||
<ModelSummaryRowView key={row.cityKey} row={row} />
|
||||
<ModelSummaryRowView key={row.cityKey} row={row} nowMs={nowMs} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
+36
-11
@@ -4,6 +4,7 @@ import {
|
||||
MODEL_SUMMARY_MODEL_COLUMNS,
|
||||
buildModelSummaryRows,
|
||||
filterModelSummaryRows,
|
||||
formatModelSummaryLocalTime,
|
||||
formatModelSummaryTemp,
|
||||
} from "@/lib/model-summary";
|
||||
|
||||
@@ -13,6 +14,22 @@ function assert(condition: unknown, message: string) {
|
||||
|
||||
export function runTests() {
|
||||
const rows = [
|
||||
{
|
||||
city: "beijing",
|
||||
city_display_name: "Beijing",
|
||||
trading_region_label: "East Asia",
|
||||
trading_region_label_zh: "东亚",
|
||||
trading_region_sort: 1,
|
||||
temp_symbol: "°C",
|
||||
current_max_so_far: 28,
|
||||
deb_prediction: 29.1,
|
||||
local_time: "18:46",
|
||||
tz_offset_seconds: 28800,
|
||||
model_cluster_sources: {
|
||||
ECMWF: 28.8,
|
||||
GFS: 29.4,
|
||||
},
|
||||
},
|
||||
{
|
||||
city: "paris",
|
||||
city_display_name: "Paris",
|
||||
@@ -68,6 +85,10 @@ export function runTests() {
|
||||
] as any;
|
||||
const originalFirstModelSources = rows[0].model_cluster_sources;
|
||||
const summaryRows = buildModelSummaryRows(rows, false);
|
||||
const amsterdamRow = summaryRows.find((row) => row.cityName === "Amsterdam");
|
||||
const beijingRow = summaryRows.find((row) => row.cityName === "Beijing");
|
||||
const madridRow = summaryRows.find((row) => row.cityName === "Madrid");
|
||||
const parisRow = summaryRows.find((row) => row.cityName === "Paris");
|
||||
|
||||
assert(
|
||||
MODEL_SUMMARY_MODEL_COLUMNS.map((column) => column.key).includes("AROME HD") &&
|
||||
@@ -75,17 +96,21 @@ export function runTests() {
|
||||
MODEL_SUMMARY_MODEL_COLUMNS.map((column) => column.key).includes("NAM"),
|
||||
"model summary must expose the fixed model columns including optional short-range models",
|
||||
);
|
||||
assert(summaryRows.length === 3, "model summary should keep one row per city");
|
||||
assert(summaryRows[0].cityName === "Amsterdam", "model summary should sort by resolved region then city name");
|
||||
assert(summaryRows[1].cityName === "Madrid", "model summary should sort by resolved region then city name");
|
||||
assert(summaryRows[2].cityName === "Paris", "model summary should sort by resolved region then city name");
|
||||
assert(summaryRows[0].regionLabel === "欧洲 / 非洲", "model summary should override stale backend timezone regions for known European cities");
|
||||
assert(summaryRows[0].localTime === "12:11", "model summary should expose local time in place of current high");
|
||||
assert(summaryRows[2].debPrediction === 31.6, "model summary should preserve DEB prediction");
|
||||
assert(summaryRows[2].models.GFS === 33.4, "model summary should preserve model high temperature");
|
||||
assert(summaryRows[2].models.HRRR === null, "missing models should be normalized to null");
|
||||
assert(summaryRows[2].modelMedian === 32.1, "model median should use available model values only");
|
||||
assert(summaryRows[2].modelSpread === 2.5, "model spread should use available model min/max only");
|
||||
assert(summaryRows.length === 4, "model summary should keep one row per city");
|
||||
assert(summaryRows[0].cityName === "Beijing", "model summary should sort by resolved region then city name");
|
||||
assert(amsterdamRow?.regionLabel === "欧洲 / 非洲", "model summary should override stale backend timezone regions for known European cities");
|
||||
if (!madridRow || !parisRow) throw new Error("model summary should keep European rows");
|
||||
assert(beijingRow?.localTime === "18:46", "model summary should keep stale source local_time only as a fallback");
|
||||
assert(
|
||||
beijingRow &&
|
||||
formatModelSummaryLocalTime(beijingRow, Date.parse("2026-06-29T12:05:00Z")) === "20:05",
|
||||
"model summary should display live local time from timezone offset instead of stale cached local_time",
|
||||
);
|
||||
assert(parisRow.debPrediction === 31.6, "model summary should preserve DEB prediction");
|
||||
assert(parisRow.models.GFS === 33.4, "model summary should preserve model high temperature");
|
||||
assert(parisRow.models.HRRR === null, "missing models should be normalized to null");
|
||||
assert(parisRow.modelMedian === 32.1, "model median should use available model values only");
|
||||
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");
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export type ModelSummaryRow = {
|
||||
regionSort: number;
|
||||
tempSymbol: string;
|
||||
localTime: string;
|
||||
timezoneOffsetSeconds: number | null;
|
||||
debPrediction: number | null;
|
||||
models: Record<ModelSummaryColumnKey, number | null>;
|
||||
modelMedian: number | null;
|
||||
@@ -110,6 +111,19 @@ export function formatModelSummaryTemp(value: number | null | undefined, symbol
|
||||
return `${numericValue.toFixed(1)}${symbol || "°C"}`;
|
||||
}
|
||||
|
||||
export function formatModelSummaryLocalTime(
|
||||
row: Pick<ModelSummaryRow, "localTime" | "timezoneOffsetSeconds">,
|
||||
nowMs: number | null | undefined = Date.now(),
|
||||
) {
|
||||
const offsetSeconds = finiteNumber(row.timezoneOffsetSeconds);
|
||||
const timestampMs = finiteNumber(nowMs);
|
||||
if (offsetSeconds == null || timestampMs == null) return row.localTime || "—";
|
||||
const localDate = new Date(timestampMs + offsetSeconds * 1000);
|
||||
const hours = String(localDate.getUTCHours()).padStart(2, "0");
|
||||
const minutes = String(localDate.getUTCMinutes()).padStart(2, "0");
|
||||
return `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
export function buildModelSummaryRows(
|
||||
rows: ScanOpportunityRow[],
|
||||
isEn: boolean,
|
||||
@@ -147,6 +161,7 @@ export function buildModelSummaryRows(
|
||||
regionSort: region.sort,
|
||||
tempSymbol: row.temp_symbol || "°C",
|
||||
localTime: normalizeLocalTime(row.local_time),
|
||||
timezoneOffsetSeconds: finiteNumber(row.tz_offset_seconds),
|
||||
debPrediction: finiteNumber(row.deb_prediction),
|
||||
models,
|
||||
modelMedian: median(modelValues),
|
||||
|
||||
Reference in New Issue
Block a user