diff --git a/monitoring-web/src/db.rs b/monitoring-web/src/db.rs index af0126b1..888f98b1 100644 --- a/monitoring-web/src/db.rs +++ b/monitoring-web/src/db.rs @@ -8,6 +8,19 @@ pub struct ObsRow { pub created_at: Option, } +/// Get today's daily max temperature for an ICAO station. +pub fn get_daily_max(db_path: &str, icao: &str) -> Option<(f64, Option)> { + let conn = Connection::open(db_path).ok()?; + let today = chrono::Utc::now().format("%Y-%m-%d").to_string(); + let mut stmt = conn + .prepare("SELECT max_temp, max_time FROM city_daily_max WHERE icao = ?1 AND obs_date = ?2") + .ok()?; + stmt.query_row(rusqlite::params![icao.to_uppercase(), today], |row| { + Ok((row.get(0)?, row.get(1)?)) + }) + .ok() +} + /// Get recent temperature observations for an ICAO station. pub fn get_recent_obs(db_path: &str, icao: &str, minutes: i32, limit: usize) -> Vec { let conn = match Connection::open(db_path) { diff --git a/monitoring-web/src/main.rs b/monitoring-web/src/main.rs index b3160308..6375b11f 100644 --- a/monitoring-web/src/main.rs +++ b/monitoring-web/src/main.rs @@ -81,10 +81,10 @@ fn load_city_snapshot(db_path: &str, idx: usize, (key, _zh, en, icao, airport, t let trend_data: Vec = temps.iter().take(6).copied().collect(); let trend = trend::calc_trend(&trend_data); - // Today's max: max of all temps in recent window (approximation) - let today_max = temps.iter().cloned().fold(None::, |a, b| { - Some(a.map_or(b, |x| x.max(b))) - }); + // Today's max: from city_daily_max table (maintained by Python collector) + let (today_max, max_time) = db::get_daily_max(db_path, icao) + .map(|(t, mt)| (Some(t), mt)) + .unwrap_or((None, None)); let new_high = match (current_temp, today_max) { (Some(ct), Some(tm)) => ct >= tm + 0.3, _ => false, @@ -111,7 +111,7 @@ fn load_city_snapshot(db_path: &str, idx: usize, (key, _zh, en, icao, airport, t local_time, current_temp, today_max, - max_time: None, + max_time, trend, new_high, runway_pairs, diff --git a/monitoring-web/static/style.css b/monitoring-web/static/style.css index 1eb2a64b..da9728f0 100644 --- a/monitoring-web/static/style.css +++ b/monitoring-web/static/style.css @@ -199,6 +199,7 @@ body { /* ── obs age ── */ .obs-age { font-size: 13px; color: #5a6170; } +.max-time { font-size: 12px; color: #4a5160; margin-left: 2px; } /* ── HTMX indicator ── */ .htmx-indicator { diff --git a/monitoring-web/templates/monitor.html b/monitoring-web/templates/monitor.html index 7c2cb5f7..9f8499e7 100644 --- a/monitoring-web/templates/monitor.html +++ b/monitoring-web/templates/monitor.html @@ -43,6 +43,21 @@
+
+ High + {% match c.today_max %} + {% when Some with (m) %} + {{ "{:.1}"|format(m) }}°C + {% match c.max_time %} + {% when Some with (mt) %} + {{ mt }} + {% when None %} + {% endmatch %} + {% when None %} + -- + {% endmatch %} + {{ c.trend.symbol() }} +
Obs {% match c.obs_age_min %} diff --git a/src/data_collection/settlement_sources.py b/src/data_collection/settlement_sources.py index 03441e84..9e464b3a 100644 --- a/src/data_collection/settlement_sources.py +++ b/src/data_collection/settlement_sources.py @@ -377,6 +377,17 @@ class SettlementSourceMixin: ) except Exception: logger.exception("airport_obs_log append failed for cwa") + try: + from src.database.db_manager import DBManager + today = datetime.now().strftime("%Y-%m-%d") + tc = payload["current"]["temp"] + if tc is not None: + DBManager().upsert_daily_max( + icao="466920", temp_c=tc, obs_date=today, + obs_time=str(obs_time_raw or "").strip(), + ) + except Exception: + pass return payload except Exception as exc: logger.warning(f"CWA settlement fetch failed: {exc}") diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index 632f1e67..bb34e59e 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -883,6 +883,19 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour ) except Exception: logger.exception("airport_obs_log append failed for metar cluster city={}", city_lower) + # 更新日最高 + try: + today = datetime.now().strftime("%Y-%m-%d") + for row in cluster_data: + icao = row.get("icao") + temp_c = row.get("temp_c") if "temp_c" in row else row.get("temp") + if icao and temp_c is not None: + DBManager().upsert_daily_max( + icao=icao, temp_c=temp_c, obs_date=today, + obs_time=str(row.get("obs_time") or ""), + ) + except Exception: + pass def _attach_china_official_nearby( self, results: Dict, city_lower: str, use_fahrenheit: bool @@ -931,6 +944,15 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour ) except Exception: logger.exception("airport_obs_log append failed for jma city={}", city_lower) + try: + today = datetime.now().strftime("%Y-%m-%d") + if icao and row.get("temp") is not None: + DBManager().upsert_daily_max( + icao=icao, temp_c=row.get("temp"), obs_date=today, + obs_time=str(row.get("obs_time") or ""), + ) + except Exception: + pass def _attach_knmi_official_nearby( self, results: Dict, city_lower: str, use_fahrenheit: bool @@ -956,6 +978,17 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour ) except Exception: logger.exception("airport_obs_log append failed for knmi city={}", city_lower) + try: + today = datetime.now().strftime("%Y-%m-%d") + icao_kn = str(rows[0].get("icao") or "EHAM") if rows else "" + temp_kn = rows[0].get("temp") if rows else None + if icao_kn and temp_kn is not None: + DBManager().upsert_daily_max( + icao=icao_kn, temp_c=temp_kn, obs_date=today, + obs_time=str(rows[0].get("obs_time") or ""), + ) + except Exception: + pass def _attach_fmi_official_nearby( self, results: Dict, city_lower: str, use_fahrenheit: bool @@ -982,6 +1015,15 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour ) except Exception: logger.exception("airport_obs_log append failed for fmi city={}", city_lower) + try: + today = datetime.now().strftime("%Y-%m-%d") + if icao and row.get("temp") is not None: + DBManager().upsert_daily_max( + icao=icao, temp_c=row.get("temp"), obs_date=today, + obs_time=str(row.get("obs_time") or ""), + ) + except Exception: + pass def _attach_hko_obs_official_nearby( self, results: Dict, city_lower: str, use_fahrenheit: bool @@ -1007,6 +1049,17 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour ) except Exception: logger.exception("airport_obs_log append failed for hko_obs city={}", city_lower) + try: + today = datetime.now().strftime("%Y-%m-%d") + icao_hk = str(rows[0].get("icao") or "HKO") if rows else "" + temp_hk = rows[0].get("temp") if rows else None + if icao_hk and temp_hk is not None: + DBManager().upsert_daily_max( + icao=icao_hk, temp_c=temp_hk, obs_date=today, + obs_time=str(rows[0].get("obs_time") or ""), + ) + except Exception: + pass def _attach_korean_amos_data( self, results: Dict, city_lower: str, use_fahrenheit: bool @@ -1047,6 +1100,18 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour ) except Exception: logger.exception("airport_obs_log append failed for amos city={}", city_lower) + # 更新日最高 + try: + today = datetime.now().strftime("%Y-%m-%d") + icao = amos_data.get("icao") or "" + temp_c = amos_data.get("temp_c") + if icao and temp_c is not None: + DBManager().upsert_daily_max( + icao=icao, temp_c=temp_c, obs_date=today, + obs_time=amos_data.get("observation_time") or "", + ) + except Exception: + pass else: logger.warning("AMOS: no data returned for city={}", city_lower) except Exception as exc: diff --git a/src/database/db_manager.py b/src/database/db_manager.py index 28447e66..57b9dea7 100644 --- a/src/database/db_manager.py +++ b/src/database/db_manager.py @@ -295,6 +295,15 @@ class DBManager: conn.execute( "CREATE INDEX IF NOT EXISTS idx_airport_obs_log_icao_time ON airport_obs_log(icao, created_at DESC)" ) + conn.execute(""" + CREATE TABLE IF NOT EXISTS city_daily_max ( + icao TEXT PRIMARY KEY, + max_temp REAL NOT NULL, + obs_date TEXT NOT NULL, + max_time TEXT, + updated_at TEXT NOT NULL + ) + """) self._ensure_column(conn, "users", "daily_points", "INTEGER DEFAULT 0") self._ensure_column(conn, "users", "daily_points_date", "TEXT") self._ensure_column(conn, "users", "weekly_points", "INTEGER DEFAULT 0") @@ -1880,3 +1889,26 @@ class DBManager: (str(icao).strip().upper(), str(-int(minutes))), ).fetchall() return [dict(r) for r in rows] + + def upsert_daily_max( + self, *, icao: str, temp_c: float, obs_date: str, + max_time: Optional[str] = None, obs_time: str = "", + ) -> None: + """Only update if temp_c exceeds stored max for the same date.""" + with self._get_connection() as conn: + row = conn.execute( + "SELECT max_temp, obs_date FROM city_daily_max WHERE icao = ?", + (str(icao).strip().upper(),), + ).fetchone() + if row and row[1] == obs_date: + if temp_c <= (row[0] or -999.0): + return # not a new high + # new day or new high: upsert + conn.execute( + """INSERT OR REPLACE INTO city_daily_max + (icao, max_temp, obs_date, max_time, updated_at) + VALUES (?, ?, ?, ?, ?)""", + (str(icao).strip().upper(), temp_c, obs_date, + max_time or "", obs_time or datetime.now().isoformat()), + ) + conn.commit()