From b7a72bd9ffcee1564275c36073d2833f6795409f Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Tue, 24 Mar 2026 16:32:43 +0800 Subject: [PATCH] Update intraday TAF summaries for active and peak windows --- frontend/lib/dashboard-utils.ts | 317 +++++++++++++++++++++----------- 1 file changed, 206 insertions(+), 111 deletions(-) diff --git a/frontend/lib/dashboard-utils.ts b/frontend/lib/dashboard-utils.ts index b39fd3f5..2dd02381 100644 --- a/frontend/lib/dashboard-utils.ts +++ b/frontend/lib/dashboard-utils.ts @@ -795,14 +795,206 @@ export function computeFrontTrendSignal( ? String(tafSignal.summary_en || "").trim() : String(tafSignal.summary_zh || "").trim() : ""; - const upperAirSummary = [baseUpperAirSummary, tafSummary] + let upperAirSummary = baseUpperAirSummary; + let tafMetric: + | { + label: string; + note: string; + tone?: string; + value: string; + } + | null = null; + let upperAirMetrics: Array<{ + label: string; + note: string; + tone?: string; + value: string; + }> = []; + const rawBackendSummary = + dateStr === detail.local_date + ? String(detail.dynamic_commentary?.summary || "").trim() + : ""; + const backendSummary = + rawBackendSummary && + (!isEnglish(locale) || !containsCjk(rawBackendSummary)) + ? rawBackendSummary + : ""; + const rawBackendNotes = Array.isArray(detail.dynamic_commentary?.notes) + ? detail.dynamic_commentary?.notes + ?.map((item) => String(item || "").trim()) + .filter(Boolean) || [] + : []; + const backendNotes = rawBackendNotes.filter( + (note) => !isEnglish(locale) || !containsCjk(note), + ); + const slice = getFutureSlice(detail, dateStr); + const currentTemp = Number(detail.current?.temp); + const currentDew = Number(detail.current?.dewpoint); + + if (!slice.length) { + return { + confidence: "low", + label: isEnglish(locale) ? "Monitoring" : "监控中", + metrics: [] as Array<{ + label: string; + note: string; + tone?: string; + value: string; + }>, + upperAirMetrics, + upperAirSummary, + precipMax: 0, + score: 0, + summary: + backendSummary || + (isEnglish(locale) + ? "Insufficient intraday structured data. Keep baseline monitoring." + : "当日日内结构化数据不足,暂时只保留基础监控。"), + weatherGovPeriods: [] as ReturnType, + }; + } + + const normalizeHm = (value: unknown): string | null => { + const match = String(value || "").match(/(\d{1,2}):(\d{2})/); + if (!match) return null; + const hour = Number.parseInt(match[1], 10); + const minute = Number.parseInt(match[2], 10); + if ( + !Number.isFinite(hour) || + !Number.isFinite(minute) || + hour < 0 || + hour > 23 || + minute < 0 || + minute > 59 + ) { + return null; + } + return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`; + }; + const hmToMinutes = (value: string | null) => { + if (!value) return null; + const [hourPart, minutePart] = value.split(":"); + const hour = Number.parseInt(hourPart || "", 10); + const minute = Number.parseInt(minutePart || "", 10); + if ( + !Number.isFinite(hour) || + !Number.isFinite(minute) || + hour < 0 || + hour > 23 || + minute < 0 || + minute > 59 + ) { + return null; + } + return hour * 60 + minute; + }; + const pointMinutes = (point: { label?: string }) => + hmToMinutes(normalizeHm(point.label)); + + const isTargetToday = dateStr === detail.local_date; + const currentHm = normalizeHm(detail.local_time); + const sunsetHm = normalizeHm(detail.forecast?.sunset); + const peakFirstHour = Number(detail.peak?.first_h); + const peakLastHour = Number(detail.peak?.last_h); + const hasPeakWindow = + Number.isFinite(peakFirstHour) && + Number.isFinite(peakLastHour) && + peakFirstHour >= 0 && + peakLastHour >= peakFirstHour; + const currentMinutes = hmToMinutes(currentHm); + const sunsetMinutes = hmToMinutes(sunsetHm); + const peakWindowStartMinutes = hasPeakWindow + ? Math.max(0, (peakFirstHour - 2) * 60) + : null; + const peakWindowEndMinutes = hasPeakWindow + ? Math.min(23 * 60 + 59, (peakLastHour + 1) * 60) + : null; + const canUseSunsetWindow = + isTargetToday && + currentMinutes !== null && + sunsetMinutes !== null && + sunsetMinutes > currentMinutes; + const tafMarkers = Array.isArray(tafSignal.markers) ? tafSignal.markers : []; + const markerSummary = ( + marker: + | { + summary_zh?: string | null; + summary_en?: string | null; + } + | null + | undefined, + ) => + !marker + ? "" + : isEnglish(locale) + ? String(marker.summary_en || "").trim() + : String(marker.summary_zh || "").trim(); + const currentTafMarker = + tafSignal.available && currentMinutes !== null + ? tafMarkers.find((marker) => { + const start = hmToMinutes(normalizeHm(marker?.start_local)); + const end = hmToMinutes(normalizeHm(marker?.end_local)); + if (start === null || end === null) return false; + return currentMinutes >= start && currentMinutes <= end; + }) + : null; + const nextTafMarker = + tafSignal.available && currentMinutes !== null && !currentTafMarker + ? tafMarkers.find((marker) => { + const start = hmToMinutes(normalizeHm(marker?.start_local)); + return start !== null && start > currentMinutes; + }) + : null; + const peakWindowTafMarker = + tafSignal.available && + peakWindowStartMinutes !== null && + peakWindowEndMinutes !== null + ? tafMarkers.find((marker) => { + const start = hmToMinutes(normalizeHm(marker?.start_local)); + const end = hmToMinutes(normalizeHm(marker?.end_local)); + if (start === null || end === null) return false; + return start <= peakWindowEndMinutes && end >= peakWindowStartMinutes; + }) + : null; + const currentTafSummary = markerSummary(currentTafMarker); + const peakTafSummary = markerSummary(peakWindowTafMarker); + const nextTafSummary = markerSummary(nextTafMarker); + const tafTimelineSummary = + tafSignal.available && dateStr === detail.local_date + ? (() => { + if (currentTafMarker && currentTafSummary) { + const range = `${normalizeHm(currentTafMarker.start_local) || "--:--"}-${normalizeHm(currentTafMarker.end_local) || "--:--"}`; + return isEnglish(locale) + ? `Current active TAF segment (${range}): ${currentTafSummary}` + : `当前生效的 TAF 时段(${range}):${currentTafSummary}`; + } + if (nextTafMarker && nextTafSummary) { + const range = `${normalizeHm(nextTafMarker.start_local) || "--:--"}-${normalizeHm(nextTafMarker.end_local) || "--:--"}`; + return isEnglish(locale) + ? `Next TAF segment (${range}): ${nextTafSummary}` + : `下一段 TAF 时段(${range}):${nextTafSummary}`; + } + return ""; + })() + : ""; + const effectivePeakTafSummary = + peakTafSummary && peakTafSummary !== currentTafSummary ? peakTafSummary : ""; + upperAirSummary = [ + baseUpperAirSummary, + tafTimelineSummary, + effectivePeakTafSummary, + tafSummary, + ] .filter(Boolean) .join(isEnglish(locale) ? " " : ""); - const tafMetric = + tafMetric = tafSignal.available && dateStr === detail.local_date ? { label: isEnglish(locale) ? "Airport TAF" : "机场预报", - note: tafSummary || + note: + tafTimelineSummary || + effectivePeakTafSummary || + tafSummary || (isEnglish(locale) ? "Airport TAF is available for the current peak window." : "当前峰值窗口已接入机场 TAF 预报。"), @@ -826,7 +1018,7 @@ export function computeFrontTrendSignal( : "暂稳", } : null; - const upperAirMetrics = upperAirSignal.source + upperAirMetrics = upperAirSignal.source ? [ ...(upperAirTradeCue ? [upperAirTradeCue] : []), { @@ -946,118 +1138,14 @@ export function computeFrontTrendSignal( ? "Medium" : "中" : isEnglish(locale) - ? "Weak" - : "弱", + ? "Weak" + : "弱", }, ...(tafMetric ? [tafMetric] : []), ] : tafMetric ? [tafMetric] : []; - const rawBackendSummary = - dateStr === detail.local_date - ? String(detail.dynamic_commentary?.summary || "").trim() - : ""; - const backendSummary = - rawBackendSummary && - (!isEnglish(locale) || !containsCjk(rawBackendSummary)) - ? rawBackendSummary - : ""; - const rawBackendNotes = Array.isArray(detail.dynamic_commentary?.notes) - ? detail.dynamic_commentary?.notes - ?.map((item) => String(item || "").trim()) - .filter(Boolean) || [] - : []; - const backendNotes = rawBackendNotes.filter( - (note) => !isEnglish(locale) || !containsCjk(note), - ); - const slice = getFutureSlice(detail, dateStr); - const currentTemp = Number(detail.current?.temp); - const currentDew = Number(detail.current?.dewpoint); - - if (!slice.length) { - return { - confidence: "low", - label: isEnglish(locale) ? "Monitoring" : "监控中", - metrics: [] as Array<{ - label: string; - note: string; - tone?: string; - value: string; - }>, - upperAirMetrics, - upperAirSummary, - precipMax: 0, - score: 0, - summary: - backendSummary || - (isEnglish(locale) - ? "Insufficient intraday structured data. Keep baseline monitoring." - : "当日日内结构化数据不足,暂时只保留基础监控。"), - weatherGovPeriods: [] as ReturnType, - }; - } - - const normalizeHm = (value: unknown): string | null => { - const match = String(value || "").match(/(\d{1,2}):(\d{2})/); - if (!match) return null; - const hour = Number.parseInt(match[1], 10); - const minute = Number.parseInt(match[2], 10); - if ( - !Number.isFinite(hour) || - !Number.isFinite(minute) || - hour < 0 || - hour > 23 || - minute < 0 || - minute > 59 - ) { - return null; - } - return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`; - }; - const hmToMinutes = (value: string | null) => { - if (!value) return null; - const [hourPart, minutePart] = value.split(":"); - const hour = Number.parseInt(hourPart || "", 10); - const minute = Number.parseInt(minutePart || "", 10); - if ( - !Number.isFinite(hour) || - !Number.isFinite(minute) || - hour < 0 || - hour > 23 || - minute < 0 || - minute > 59 - ) { - return null; - } - return hour * 60 + minute; - }; - const pointMinutes = (point: { label?: string }) => - hmToMinutes(normalizeHm(point.label)); - - const isTargetToday = dateStr === detail.local_date; - const currentHm = normalizeHm(detail.local_time); - const sunsetHm = normalizeHm(detail.forecast?.sunset); - const peakFirstHour = Number(detail.peak?.first_h); - const peakLastHour = Number(detail.peak?.last_h); - const hasPeakWindow = - Number.isFinite(peakFirstHour) && - Number.isFinite(peakLastHour) && - peakFirstHour >= 0 && - peakLastHour >= peakFirstHour; - const currentMinutes = hmToMinutes(currentHm); - const sunsetMinutes = hmToMinutes(sunsetHm); - const peakWindowStartMinutes = hasPeakWindow - ? Math.max(0, (peakFirstHour - 2) * 60) - : null; - const peakWindowEndMinutes = hasPeakWindow - ? Math.min(23 * 60 + 59, (peakLastHour + 1) * 60) - : null; - const canUseSunsetWindow = - isTargetToday && - currentMinutes !== null && - sunsetMinutes !== null && - sunsetMinutes > currentMinutes; const futureSlice = isTargetToday && currentMinutes !== null @@ -1359,7 +1447,14 @@ export function computeFrontTrendSignal( : ""; const backendSupplement = backendSummary && backendSummary !== summary ? backendSummary : ""; - const combinedSummary = [summary, tafSummary, tafContrastSummary, backendSupplement] + const combinedSummary = [ + summary, + tafTimelineSummary, + effectivePeakTafSummary, + tafSummary, + tafContrastSummary, + backendSupplement, + ] .filter(Boolean) .join(isEnglish(locale) ? " " : ""); const cloudNote = (() => {