移除未使用的 Groq 和 Meteoblue 服务代码及配置
This commit is contained in:
@@ -4,7 +4,6 @@ import {
|
||||
CityDetail,
|
||||
CityListItem,
|
||||
CitySummary,
|
||||
HistoryPayload,
|
||||
MarketScan,
|
||||
ScanTerminalFilters,
|
||||
ScanTerminalResponse,
|
||||
@@ -20,7 +19,6 @@ const CACHE_TTL_MS = 30 * 60 * 1000;
|
||||
const SCAN_TERMINAL_CLIENT_TIMEOUT_MS = 35_000;
|
||||
const CITY_DETAIL_CLIENT_TIMEOUT_MS = 35_000;
|
||||
const pendingCityDetailRequests = new Map<string, Promise<CityDetail>>();
|
||||
const pendingHistoryRequests = new Map<string, Promise<HistoryPayload>>();
|
||||
const pendingCitySummaryRequests = new Map<string, Promise<CitySummary>>();
|
||||
const pendingCityMarketScanRequests = new Map<
|
||||
string,
|
||||
@@ -452,43 +450,6 @@ export const dashboardClient = {
|
||||
return request;
|
||||
},
|
||||
|
||||
async getHistory(cityName: string, options?: { includeRecords?: boolean }) {
|
||||
const includeRecords = options?.includeRecords === true;
|
||||
const requestKey = `${normalizeCityName(cityName)}::${
|
||||
includeRecords ? "full" : "preview"
|
||||
}`;
|
||||
const existing = pendingHistoryRequests.get(requestKey);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (includeRecords) {
|
||||
params.set("include_records", "true");
|
||||
}
|
||||
|
||||
const request = fetchJson<HistoryPayload>(
|
||||
`/api/history/${normalizeCityName(cityName)}${
|
||||
params.size ? `?${params.toString()}` : ""
|
||||
}`,
|
||||
)
|
||||
.then((data) => ({
|
||||
...data,
|
||||
full_count: Number(data.full_count || 0),
|
||||
has_more: data.has_more === true,
|
||||
history: Array.isArray(data.history) ? data.history : [],
|
||||
mode: (data.mode === "full" ? "full" : "preview") as
|
||||
| "full"
|
||||
| "preview",
|
||||
preview_count: Number(data.preview_count || 0),
|
||||
}))
|
||||
.finally(() => {
|
||||
pendingHistoryRequests.delete(requestKey);
|
||||
});
|
||||
|
||||
pendingHistoryRequests.set(requestKey, request);
|
||||
return request;
|
||||
},
|
||||
|
||||
isCityDetailFresh(meta?: CityCacheMeta | null) {
|
||||
return isFresh(meta);
|
||||
|
||||
@@ -976,78 +976,13 @@ export interface AmosData {
|
||||
observation_time_local?: string | null;
|
||||
}
|
||||
|
||||
export interface HistoryPoint {
|
||||
date: string;
|
||||
actual: number | null;
|
||||
deb: number | null;
|
||||
mu?: number | null;
|
||||
mgm?: number | null;
|
||||
forecasts?: Record<string, number | null>;
|
||||
model_reference?: {
|
||||
available?: boolean;
|
||||
truth_layer?: string | null;
|
||||
reference_layer?: string | null;
|
||||
deb?: {
|
||||
value?: number | null;
|
||||
error?: number | null;
|
||||
};
|
||||
models?: Array<{
|
||||
model?: string | null;
|
||||
value?: number | null;
|
||||
error?: number | null;
|
||||
participates_in_deb?: boolean;
|
||||
}>;
|
||||
model_count?: number | null;
|
||||
};
|
||||
settlement_source?: string | null;
|
||||
settlement_station_code?: string | null;
|
||||
settlement_station_label?: string | null;
|
||||
truth_version?: string | null;
|
||||
updated_by?: string | null;
|
||||
truth_updated_at?: number | null;
|
||||
actual_peak_time?: string | null;
|
||||
deb_at_peak_minus_12h?: number | null;
|
||||
deb_at_peak_minus_12h_time?: string | null;
|
||||
deb_at_peak_minus_12h_error?: number | null;
|
||||
}
|
||||
|
||||
export interface HistoryPayloadMeta {
|
||||
mode: "preview" | "full";
|
||||
hasMore: boolean;
|
||||
fullCount: number;
|
||||
previewCount: number;
|
||||
settlementSource?: string | null;
|
||||
settlementSourceLabel?: string | null;
|
||||
}
|
||||
|
||||
export interface HistoryPayload {
|
||||
history: HistoryPoint[];
|
||||
has_more?: boolean;
|
||||
full_count?: number;
|
||||
preview_count?: number;
|
||||
mode?: "preview" | "full";
|
||||
settlement_source?: string | null;
|
||||
settlement_source_label?: string | null;
|
||||
}
|
||||
|
||||
export interface LoadingState {
|
||||
cities: boolean;
|
||||
cityDetail: boolean;
|
||||
refresh: boolean;
|
||||
history: boolean;
|
||||
marketScan?: boolean;
|
||||
futureDeep?: boolean;
|
||||
historyRecords?: boolean;
|
||||
}
|
||||
refresh: boolean; marketScan?: boolean;
|
||||
futureDeep?: boolean;}
|
||||
|
||||
export interface HistoryState {
|
||||
isOpen: boolean;
|
||||
loading: boolean;
|
||||
recordsLoading: boolean;
|
||||
error: string | null;
|
||||
dataByCity: Record<string, HistoryPoint[]>;
|
||||
metaByCity: Record<string, HistoryPayloadMeta>;
|
||||
}
|
||||
|
||||
export interface ProAccessState {
|
||||
loading: boolean;
|
||||
@@ -1073,6 +1008,5 @@ export interface DashboardState {
|
||||
selectedForecastDate: string | null;
|
||||
forecastModalMode: ForecastModalMode | null;
|
||||
loadingState: LoadingState;
|
||||
historyState: HistoryState;
|
||||
proAccess: ProAccessState;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Locale } from "@/lib/i18n";
|
||||
import {
|
||||
AiAnalysisStructured,
|
||||
CityDetail,
|
||||
HistoryPoint,
|
||||
NearbyStation,
|
||||
} from "@/lib/dashboard-types";
|
||||
import {
|
||||
@@ -1715,142 +1714,6 @@ export function getShortTermNowcastLines(
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function getHistorySummary(
|
||||
history: HistoryPoint[],
|
||||
cityLocalDate?: string | null,
|
||||
) {
|
||||
const toFinite = (value: unknown): number | null => {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : null;
|
||||
};
|
||||
const isExcludedModel = (name: string) =>
|
||||
String(name || "").toLowerCase().includes("meteoblue");
|
||||
|
||||
const cutoff = new Date();
|
||||
cutoff.setHours(0, 0, 0, 0);
|
||||
cutoff.setDate(cutoff.getDate() - 14);
|
||||
|
||||
const recentData = history.filter((row) => {
|
||||
if (!row?.date) return false;
|
||||
const rowDate = new Date(`${row.date}T00:00:00`);
|
||||
return !Number.isNaN(rowDate.getTime()) && rowDate >= cutoff;
|
||||
});
|
||||
|
||||
const settledData = recentData.filter((row) => {
|
||||
if (!row?.date) return false;
|
||||
return cityLocalDate
|
||||
? row.date < cityLocalDate
|
||||
: row.date < new Date().toISOString().slice(0, 10);
|
||||
});
|
||||
const comparableSettledData = settledData.filter((row) => {
|
||||
const actual = toFinite(row.actual);
|
||||
const deb = toFinite(row.deb);
|
||||
return actual != null && deb != null;
|
||||
});
|
||||
|
||||
let hits = 0;
|
||||
const debErrors: number[] = [];
|
||||
const modelErrors: Record<string, number[]> = {};
|
||||
|
||||
comparableSettledData.forEach((row) => {
|
||||
const actual = toFinite(row.actual);
|
||||
const deb = toFinite(row.deb);
|
||||
if (actual == null || deb == null) return;
|
||||
debErrors.push(Math.abs(actual - deb));
|
||||
if (wuRound(actual) === wuRound(deb)) {
|
||||
hits += 1;
|
||||
}
|
||||
|
||||
const forecasts = row.forecasts || {};
|
||||
Object.entries(forecasts).forEach(([modelName, modelValue]) => {
|
||||
if (isExcludedModel(modelName)) return;
|
||||
const mv = toFinite(modelValue);
|
||||
if (actual == null || mv == null) return;
|
||||
if (!modelErrors[modelName]) {
|
||||
modelErrors[modelName] = [];
|
||||
}
|
||||
modelErrors[modelName].push(Math.abs(actual - mv));
|
||||
});
|
||||
});
|
||||
|
||||
const modelMaeList = Object.entries(modelErrors)
|
||||
.map(([name, errors]) => ({
|
||||
mae:
|
||||
errors.length > 0
|
||||
? errors.reduce((sum, value) => sum + value, 0) / errors.length
|
||||
: Number.POSITIVE_INFINITY,
|
||||
model: name,
|
||||
sampleCount: errors.length,
|
||||
}))
|
||||
.filter((row) => Number.isFinite(row.mae) && row.sampleCount > 0)
|
||||
.sort((a, b) => a.mae - b.mae);
|
||||
|
||||
const primaryModelMaeList = modelMaeList.filter((row) => row.sampleCount >= 2);
|
||||
const bestModel = (primaryModelMaeList[0] || modelMaeList[0]) ?? null;
|
||||
const bestModelName = bestModel?.model || null;
|
||||
const bestModelMae = bestModel ? Number(bestModel.mae.toFixed(1)) : null;
|
||||
const bestModelSeries = recentData.map((row) =>
|
||||
bestModelName ? toFinite(row.forecasts?.[bestModelName]) : null,
|
||||
);
|
||||
|
||||
let debWinDaysVsBest = 0;
|
||||
let debVsBestComparableDays = 0;
|
||||
if (bestModelName) {
|
||||
comparableSettledData.forEach((row) => {
|
||||
const actual = toFinite(row.actual);
|
||||
const deb = toFinite(row.deb);
|
||||
const bestModelVal = toFinite(row.forecasts?.[bestModelName]);
|
||||
if (actual == null || deb == null || bestModelVal == null) return;
|
||||
debVsBestComparableDays += 1;
|
||||
if (Math.abs(deb - actual) <= Math.abs(bestModelVal - actual)) {
|
||||
debWinDaysVsBest += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const mgmSettledCount = settledData.reduce((count, row) => {
|
||||
return toFinite(row.mgm) != null ? count + 1 : count;
|
||||
}, 0);
|
||||
const mgmSeriesComplete =
|
||||
settledData.length >= 2 && mgmSettledCount === settledData.length;
|
||||
const mgmSeries = mgmSeriesComplete
|
||||
? recentData.map((row) => row.mgm ?? null)
|
||||
: recentData.map(() => null);
|
||||
|
||||
return {
|
||||
dates: recentData.map((row) => row.date),
|
||||
debMae: debErrors.length
|
||||
? Number(
|
||||
(
|
||||
debErrors.reduce((sum, value) => sum + value, 0) / debErrors.length
|
||||
).toFixed(1),
|
||||
)
|
||||
: null,
|
||||
debs: recentData.map((row) => row.deb),
|
||||
bestModelName,
|
||||
bestModelMae,
|
||||
bestModelSeries,
|
||||
modelMaeRanks: modelMaeList.map((row) => ({
|
||||
model: row.model,
|
||||
mae: Number(row.mae.toFixed(1)),
|
||||
sampleCount: row.sampleCount,
|
||||
})),
|
||||
debWinDaysVsBest,
|
||||
debVsBestComparableDays,
|
||||
debWinRateVsBest:
|
||||
debVsBestComparableDays > 0
|
||||
? Number(((debWinDaysVsBest / debVsBestComparableDays) * 100).toFixed(0))
|
||||
: null,
|
||||
hitRate: debErrors.length
|
||||
? Number(((hits / debErrors.length) * 100).toFixed(0))
|
||||
: null,
|
||||
mgmSeriesComplete,
|
||||
mgms: mgmSeries,
|
||||
recentData,
|
||||
settledCount: comparableSettledData.length,
|
||||
actuals: recentData.map((row) => row.actual),
|
||||
};
|
||||
}
|
||||
|
||||
function toFiniteNumber(value: unknown): number | null {
|
||||
const numeric = Number(value);
|
||||
|
||||
@@ -44,7 +44,6 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
|
||||
"detail.closeAria": "关闭城市详情面板",
|
||||
"detail.waitSelect": "等待选择城市",
|
||||
"detail.todayAnalysis": "今日日内分析",
|
||||
"detail.history": "历史对账",
|
||||
"detail.loading": "正在加载城市详情...",
|
||||
"detail.emptyHint": "从左侧城市列表选择一个城市查看详情。",
|
||||
"detail.sceneryAlt": "{city} 风景照",
|
||||
@@ -72,22 +71,6 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
|
||||
"guide.footer":
|
||||
"数据源以 METAR、香港天文台(HKO)、NOAA 指定站点、Turkish MGM、Open-Meteo、weather.gov 为主。",
|
||||
|
||||
"history.title": "📊 历史准确率对账 - {city}",
|
||||
"history.closeAria": "关闭历史对账",
|
||||
"history.loading": "正在获取历史数据...",
|
||||
"history.error": "获取历史信息失败",
|
||||
"history.empty": "近 15 天暂无该城市历史数据",
|
||||
"history.previewTitle": "历史准确率对账",
|
||||
"history.previewDesc": "对比 DEB 预报与实际结算温度,查看命中率、MAE 和模型对比。升级 Pro 即可解锁。",
|
||||
"history.hitRate": "DEB 结算胜率 (METAR)",
|
||||
"history.mae": "DEB MAE",
|
||||
"history.debHitRate": "DEB 结算胜率 (METAR)",
|
||||
"history.debMae": "DEB MAE",
|
||||
"history.muMae": "μ MAE",
|
||||
"history.bestModelMae": "最佳单模型 MAE",
|
||||
"history.debVsBest": "DEB 优于最佳模型",
|
||||
"history.sample": "近 15 天已结算样本",
|
||||
"history.sampleDays": "{count} 天",
|
||||
|
||||
"future.todayTitle": "{city} · 今日日内分析",
|
||||
"future.dateTitle": "{city} · {date} 未来日期分析",
|
||||
@@ -242,7 +225,6 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
|
||||
"detail.closeAria": "Close city detail panel",
|
||||
"detail.waitSelect": "Waiting for city selection",
|
||||
"detail.todayAnalysis": "Today's Intraday",
|
||||
"detail.history": "History Reconciliation",
|
||||
"detail.loading": "Loading city details...",
|
||||
"detail.emptyHint": "Select a city from the left list to view details.",
|
||||
"detail.sceneryAlt": "{city} scenery",
|
||||
@@ -271,22 +253,6 @@ const MESSAGES: Record<Locale, Record<string, string>> = {
|
||||
"guide.footer":
|
||||
"Primary data sources are METAR, Hong Kong Observatory (HKO), designated NOAA stations, Turkish MGM, Open-Meteo, and weather.gov.",
|
||||
|
||||
"history.title": "📊 Historical Reconciliation - {city}",
|
||||
"history.closeAria": "Close history reconciliation",
|
||||
"history.loading": "Loading historical data...",
|
||||
"history.error": "Failed to load historical data",
|
||||
"history.empty": "No historical records for this city in the last 15 days",
|
||||
"history.previewTitle": "Historical Reconciliation",
|
||||
"history.previewDesc": "Compare DEB forecasts to actual settlement temperatures with hit rate, MAE, and model comparison. Unlock Pro to access.",
|
||||
"history.hitRate": "DEB Settlement Hit Rate (METAR)",
|
||||
"history.mae": "DEB MAE",
|
||||
"history.debHitRate": "DEB Settlement Hit Rate (METAR)",
|
||||
"history.debMae": "DEB MAE",
|
||||
"history.muMae": "μ MAE",
|
||||
"history.bestModelMae": "Best Single-model MAE",
|
||||
"history.debVsBest": "DEB vs Best Model",
|
||||
"history.sample": "Settled Samples (Last 15 Days)",
|
||||
"history.sampleDays": "{count} days",
|
||||
|
||||
"future.todayTitle": "{city} · Intraday Analysis",
|
||||
"future.dateTitle": "{city} · {date} Future-date Analysis",
|
||||
|
||||
Reference in New Issue
Block a user