diff --git a/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts index 1335c627..fba50346 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/temperatureDefaultVisibilityPolicy.test.ts @@ -161,6 +161,56 @@ export function runTests() { "DEB peak auto window should not expand beyond 12 hours", ); + const postPeakWindowChart = __buildTemperatureChartDataForTest( + { + city: "beijing", + local_date: "2026-05-26", + local_time: "21:10", + tz_offset_seconds: 8 * 60 * 60, + deb_prediction: 35, + runway_plate_history: { + "19/01": [ + { time: "2026-05-26T10:00:00+08:00", temp: 29.8 }, + { time: "2026-05-26T15:00:00+08:00", temp: 34.9 }, + { time: "2026-05-26T21:00:00+08:00", temp: 28.6 }, + ], + }, + } as any, + { + localTime: "21:10", + times: [ + "00:00", "01:00", "02:00", "03:00", "04:00", "05:00", + "06:00", "07:00", "08:00", "09:00", "10:00", "11:00", + "12:00", "13:00", "14:00", "15:00", "16:00", "17:00", + "18:00", "19:00", "20:00", "21:00", "22:00", "23:00", + ], + temps: [ + 20, 20.5, 21, 21.5, 22, 23, + 24, 25, 26, 27, 29, 31, + 32, 33, 34.2, 35, 34.4, 33.3, + 31.8, 30.2, 28.5, 27, 25.5, 24, + ], + debPrediction: 35, + } as any, + "1D", + ); + const postPeakWindowRange = __getDebPeakWindowRangeForTest( + postPeakWindowChart.data, + postPeakWindowChart.series as any, + ); + assert(postPeakWindowRange, "post-peak default chart view should still derive from the DEB peak window"); + const postPeakWindowRows = postPeakWindowChart.data.slice(postPeakWindowRange![0], postPeakWindowRange![1] + 1); + const postPeakWindowStart = postPeakWindowRows[0].ts; + const postPeakWindowEnd = postPeakWindowRows[postPeakWindowRows.length - 1].ts; + assert( + postPeakWindowEnd >= Date.UTC(2026, 4, 26, 21, 0, 0), + "After the peak window, default high-temperature view should extend to the latest live observation", + ); + assert( + postPeakWindowEnd - postPeakWindowStart <= 12 * 60 * 60 * 1000, + "Post-peak high-temperature view should keep a bounded 12-hour window", + ); + assert( __isTemperatureSeriesVisibleByDefaultForTest("paris", "model_curve_AROME HD"), "Paris AROME HD should be the only default-visible model curve exception", diff --git a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts index 3cdcf27e..dbfbf072 100644 --- a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts +++ b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts @@ -1445,6 +1445,33 @@ function buildChartDomain( return [Number((min - pad).toFixed(1)), Number((max + pad).toFixed(1))]; } +function isLiveObservationSeries(series: EvidenceSeries) { + if (series.key === "hourly_forecast") return false; + if (series.key.startsWith("model_curve_")) return false; + if (series.key.startsWith("model_summary_")) return false; + if (["deb_prediction", "max_temp", "min_temp"].includes(series.key)) return false; + + const source = String(series.source || "").toLowerCase(); + if (source.includes("forecast") || source.includes("multi-model") || source === "deb") return false; + return true; +} + +function latestLiveObservationTimestamp( + data: Array>, + series: EvidenceSeries[], +) { + let latest: number | null = null; + series.filter(isLiveObservationSeries).forEach((item) => { + item.values.forEach((value, index) => { + if (validNumber(value) === null) return; + const ts = typeof data[index]?.ts === "number" ? data[index].ts : null; + if (ts === null) return; + latest = latest === null ? ts : Math.max(latest, ts); + }); + }); + return latest; +} + function getDebPeakWindowRange( data: Array>, series: EvidenceSeries[], @@ -1487,6 +1514,7 @@ function getDebPeakWindowRange( let startTs = debPoints[hotStartPoint].ts - 1.5 * hour; let endTs = debPoints[hotEndPoint].ts + 2 * hour; const centerTs = peak.ts; + const latestObsTs = latestLiveObservationTimestamp(data, series); if (endTs - startTs < targetSpan) { startTs = centerTs - targetSpan / 2; @@ -1496,6 +1524,12 @@ function getDebPeakWindowRange( startTs = centerTs - maxSpan / 2; endTs = centerTs + maxSpan / 2; } + if (latestObsTs !== null && latestObsTs > endTs && latestObsTs > debPoints[hotEndPoint].ts) { + endTs = Math.min(lastTs, latestObsTs); + if (endTs - startTs > maxSpan) { + startTs = Math.max(firstTs, endTs - maxSpan); + } + } if (startTs < firstTs) { endTs = Math.min(lastTs, endTs + firstTs - startTs);