2b1d7c0b65
The dashboard had several oversized orchestration, component, and CSS files that made product-copy changes and mobile/performance work risky. This refactor preserves behavior while splitting scan terminal CSS, opportunity helpers, future forecast panels, history/detail charts, and probability/model sections into smaller ownership boundaries. Constraint: No user-visible version bump because this batch is architecture and performance cleanup, not a release announcement. Rejected: Rewrite dashboard state management in the same batch | too broad for a safe upload after CSS and component splitting. Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep new component/CSS boundaries instead of moving product copy back into the large dashboard files. Tested: npm run build; npm run test:business; git diff --check Not-tested: Browser visual smoke test after push
128 lines
5.8 KiB
TypeScript
128 lines
5.8 KiB
TypeScript
import type { CityDetail, ScanOpportunityRow } from "@/lib/dashboard-types";
|
|
import { formatTemperatureValue } from "@/lib/temperature-utils";
|
|
import { formatTemperatureDelta } from "./opportunity-format";
|
|
import { getMetarObservationContext } from "./opportunity-observation";
|
|
import { getTargetRange } from "./opportunity-target";
|
|
|
|
export function getDebDistanceSummary(
|
|
row: ScanOpportunityRow,
|
|
locale: string,
|
|
tempSymbol?: string | null,
|
|
) {
|
|
const deb =
|
|
row.deb_prediction != null && Number.isFinite(Number(row.deb_prediction))
|
|
? Number(row.deb_prediction)
|
|
: null;
|
|
if (deb == null) return locale === "en-US" ? "DEB pending" : "DEB 待确认";
|
|
const { lower, upper } = getTargetRange(row);
|
|
if (lower != null && upper == null) {
|
|
const delta = deb - lower;
|
|
if (Math.abs(delta) < 0.05) return locale === "en-US" ? "DEB on threshold" : "DEB 贴近阈值";
|
|
return delta >= 0
|
|
? locale === "en-US"
|
|
? `DEB above by ${formatTemperatureDelta(delta, tempSymbol)}`
|
|
: `DEB 高于阈值 ${formatTemperatureDelta(delta, tempSymbol)}`
|
|
: locale === "en-US"
|
|
? `DEB below by ${formatTemperatureDelta(delta, tempSymbol)}`
|
|
: `DEB 低于阈值 ${formatTemperatureDelta(delta, tempSymbol)}`;
|
|
}
|
|
if (upper != null && lower == null) {
|
|
const delta = deb - upper;
|
|
if (Math.abs(delta) < 0.05) return locale === "en-US" ? "DEB on threshold" : "DEB 贴近阈值";
|
|
return delta <= 0
|
|
? locale === "en-US"
|
|
? `DEB below by ${formatTemperatureDelta(delta, tempSymbol)}`
|
|
: `DEB 低于阈值 ${formatTemperatureDelta(delta, tempSymbol)}`
|
|
: locale === "en-US"
|
|
? `DEB above by ${formatTemperatureDelta(delta, tempSymbol)}`
|
|
: `DEB 高于阈值 ${formatTemperatureDelta(delta, tempSymbol)}`;
|
|
}
|
|
if (lower != null && upper != null) {
|
|
if (deb >= lower && deb <= upper) return locale === "en-US" ? "DEB inside bucket" : "DEB 位于桶内";
|
|
const nearest = deb < lower ? lower : upper;
|
|
const delta = deb - nearest;
|
|
return deb < lower
|
|
? locale === "en-US"
|
|
? `DEB below bucket by ${formatTemperatureDelta(delta, tempSymbol)}`
|
|
: `DEB 低于桶 ${formatTemperatureDelta(delta, tempSymbol)}`
|
|
: locale === "en-US"
|
|
? `DEB above bucket by ${formatTemperatureDelta(delta, tempSymbol)}`
|
|
: `DEB 高于桶 ${formatTemperatureDelta(delta, tempSymbol)}`;
|
|
}
|
|
return locale === "en-US"
|
|
? `DEB ${formatTemperatureValue(deb, tempSymbol, { digits: 1 })}`
|
|
: `DEB ${formatTemperatureValue(deb, tempSymbol, { digits: 1 })}`;
|
|
}
|
|
|
|
export function getModelSupportSummary(
|
|
row: ScanOpportunityRow,
|
|
locale: string,
|
|
) {
|
|
const sources = Object.values(row.model_cluster_sources || {})
|
|
.map((value) => Number(value))
|
|
.filter((value) => Number.isFinite(value));
|
|
const deb =
|
|
row.deb_prediction != null && Number.isFinite(Number(row.deb_prediction))
|
|
? Number(row.deb_prediction)
|
|
: null;
|
|
if (!sources.length || deb == null) return locale === "en-US" ? "Models pending" : "模型待确认";
|
|
const { lower, upper } = getTargetRange(row);
|
|
let supports = 0;
|
|
if (lower != null && upper == null) {
|
|
supports = sources.filter((value) => (deb >= lower ? value >= lower : value < lower)).length;
|
|
} else if (upper != null && lower == null) {
|
|
supports = sources.filter((value) => (deb <= upper ? value <= upper : value > upper)).length;
|
|
} else if (lower != null && upper != null) {
|
|
if (deb >= lower && deb <= upper) {
|
|
supports = sources.filter((value) => value >= lower && value <= upper).length;
|
|
} else if (deb < lower) {
|
|
supports = sources.filter((value) => value < lower).length;
|
|
} else {
|
|
supports = sources.filter((value) => value > upper).length;
|
|
}
|
|
} else {
|
|
const tolerance = 1;
|
|
supports = sources.filter((value) => Math.abs(value - deb) <= tolerance).length;
|
|
}
|
|
return locale === "en-US"
|
|
? `${supports}/${sources.length} models support DEB`
|
|
: `${supports}/${sources.length} 模型支持 DEB`;
|
|
}
|
|
|
|
export function getMetarConflictSummary(
|
|
row: ScanOpportunityRow,
|
|
detail: CityDetail | null,
|
|
locale: string,
|
|
) {
|
|
const obs = getMetarObservationContext(row, detail);
|
|
if (obs.stale || obs.maxTemp == null) return locale === "en-US" ? "METAR pending" : "METAR 待确认";
|
|
const deb =
|
|
row.deb_prediction != null && Number.isFinite(Number(row.deb_prediction))
|
|
? Number(row.deb_prediction)
|
|
: null;
|
|
const { lower, upper } = getTargetRange(row);
|
|
if (deb == null || (lower == null && upper == null)) {
|
|
return locale === "en-US" ? "METAR read only" : "METAR 仅参考";
|
|
}
|
|
const phase = String(row.window_phase || "").toLowerCase();
|
|
const peakPending =
|
|
phase === "early_today" ||
|
|
phase === "setup_today" ||
|
|
(row.minutes_until_peak_start != null && Number(row.minutes_until_peak_start) > 0);
|
|
if (lower != null && upper == null) {
|
|
if (deb < lower && obs.maxTemp >= lower) return locale === "en-US" ? "METAR conflicts" : "METAR 冲突";
|
|
if (deb >= lower && obs.maxTemp < lower && peakPending) return locale === "en-US" ? "Await peak" : "等待峰值";
|
|
return locale === "en-US" ? "METAR no conflict" : "METAR 未冲突";
|
|
}
|
|
if (upper != null && lower == null) {
|
|
if (deb <= upper && obs.maxTemp > upper) return locale === "en-US" ? "METAR conflicts" : "METAR 冲突";
|
|
if (deb > upper && obs.maxTemp <= upper && peakPending) return locale === "en-US" ? "Await peak" : "等待峰值";
|
|
return locale === "en-US" ? "METAR no conflict" : "METAR 未冲突";
|
|
}
|
|
if (lower != null && upper != null && deb >= lower && deb <= upper) {
|
|
if (obs.maxTemp > upper) return locale === "en-US" ? "METAR above bucket" : "METAR 已越过桶";
|
|
if (obs.maxTemp < lower && peakPending) return locale === "en-US" ? "Await peak" : "等待峰值";
|
|
}
|
|
return locale === "en-US" ? "METAR no conflict" : "METAR 未冲突";
|
|
}
|