"use client"; import { useMemo, useState, useEffect } from "react"; import { useDashboardStore } from "@/hooks/useDashboardStore"; import type { CityDetail } from "@/lib/dashboard-types"; const MONITOR_KEYS = [ "seoul", "busan", "tokyo", "ankara", "helsinki", "amsterdam", "istanbul", "paris", "hong kong", "lau fau shan", "taipei", "new york", "los angeles", "chicago", "denver", "atlanta", "miami", "san francisco", "houston", "dallas", "austin", "seattle", ] as const; type MonitorCity = { key: string; detail: CityDetail | undefined; }; function fmt(v: number | null | undefined): string { return v != null ? v.toFixed(1) : "--"; } function trendIcon(detail: CityDetail | undefined): { s: string; c: string } { if (!detail?.airport_current) return { s: "→", c: "flat" }; const ac = detail.airport_current; const cur = ac.temp ?? detail.current?.temp ?? null; const max = ac.max_so_far ?? null; if (cur != null && max != null && cur >= max + 0.3) return { s: "↑", c: "rising" }; if (cur != null && max != null && cur < max - 1.0) return { s: "↓", c: "falling" }; return { s: "→", c: "flat" }; } export default function MonitorPanel() { const store = useDashboardStore(); const details = store.cityDetailsByName; const [time, setTime] = useState(""); useEffect(() => { setTime(new Date().toLocaleTimeString()); const t = setInterval(() => setTime(new Date().toLocaleTimeString()), 60_000); return () => clearInterval(t); }, []); const [notify, setNotify] = useState(() => { if (typeof window === "undefined") return false; return localStorage.getItem("monitor_notify") !== "off"; }); // 1 min force-refresh all 11 monitoring cities useEffect(() => { let cancelled = false; async function refreshAll() { for (const k of MONITOR_KEYS) { if (cancelled) break; try { await store.ensureCityDetail(k, true, "panel"); } catch {} } } refreshAll(); const t = setInterval(refreshAll, 60_000); return () => { cancelled = true; clearInterval(t); }; }, [store.ensureCityDetail]); const cities: MonitorCity[] = useMemo(() => { return MONITOR_KEYS.map((k) => ({ key: k, detail: details[k] })); }, [details]); // Sort by temp descending const sorted = useMemo(() => { return [...cities].sort((a, b) => { const ta = a.detail?.airport_current?.temp ?? a.detail?.current?.temp ?? null; const tb = b.detail?.airport_current?.temp ?? b.detail?.current?.temp ?? null; if (ta == null && tb == null) return 0; if (ta == null) return 1; if (tb == null) return -1; return tb - ta; }); }, [cities]); const toggleNotify = () => { const next = !notify; setNotify(next); localStorage.setItem("monitor_notify", next ? "on" : "off"); if (next && typeof Notification !== "undefined" && Notification.permission === "default") { Notification.requestPermission(); } }; // Check for new highs and fire notifications useEffect(() => { if (!notify || typeof Notification === "undefined" || Notification.permission !== "granted") return; for (const c of sorted) { const ac = c.detail?.airport_current; const cur = ac?.temp ?? c.detail?.current?.temp ?? null; const max = ac?.max_so_far ?? null; if (cur != null && max != null && cur >= max + 0.3) { const key = `${c.key}|${cur}`; const today = new Date().toDateString(); let notified: Record = {}; try { notified = JSON.parse(localStorage.getItem("monitor_notified_highs") || "{}"); } catch {} if (notified._day !== today) notified = { _day: today }; if (!notified[key]) { notified[key] = true; localStorage.setItem("monitor_notified_highs", JSON.stringify(notified)); const name = c.detail?.display_name || c.key; new Notification(`🔴 New High — ${name}`, { body: `${cur}°C\nNew daily high.`, tag: key, requireInteraction: true, }); } } } }, [sorted, notify]); const airportName = (key: string): string => { const m: Record = { seoul: "Incheon", busan: "Gimhae", tokyo: "Haneda", ankara: "Esenboğa", helsinki: "Vantaa", amsterdam: "Schiphol", istanbul: "Airport", paris: "Le Bourget", "hong kong": "Observatory", "lau fau shan": "Lau Fau Shan", taipei: "Songshan", "new york": "LaGuardia", "los angeles": "LAX", chicago: "O'Hare", denver: "Buckley", atlanta: "Hartsfield", miami: "MIA", "san francisco": "SFO", houston: "Hobby", dallas: "Love Field", austin: "Bergstrom", seattle: "SeaTac", }; return m[key] || ""; }; return (

🔥 市场监控

{time}
{sorted.map((c) => { const ac = c.detail?.airport_current; const cur = ac?.temp ?? c.detail?.current?.temp ?? null; const max = ac?.max_so_far ?? null; const mtt = ac?.max_temp_time ?? null; const obs = ac?.obs_time ?? c.detail?.local_time ?? ""; const age = ac?.obs_age_min ?? null; const newHigh = cur != null && max != null && cur >= max + 0.3; const warm = cur != null && cur >= 30; const tr = trendIcon(c.detail); const rw = c.detail?.amos?.runway_obs; const rwPairs = rw?.runway_pairs || []; const rwTemps = rw?.temperatures || []; return (
{c.detail?.display_name || c.key} / {airportName(c.key)} {obs} {newHigh && ( ◆新高 )}
{cur != null ? ( <> {cur.toFixed(1)} °C ) : ( -- )}
High {max != null ? ( <> {max.toFixed(1)}°C {mtt && {mtt}} ) : ( -- )} {tr.s}
Obs {age != null ? ( {age} min ago ) : ( -- )}
{rwPairs.length > 0 && rwTemps.length > 0 && (
{rwPairs.map((p, i) => { const t = rwTemps[i]?.[0]; if (t == null) return null; return (
{p[0]}/{p[1]} {t.toFixed(1)}°C
); })}
)}
); })}
); }