diff --git a/requirements.txt b/requirements.txt index e25f9783..9a79aed8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,5 +10,4 @@ lightgbm web3 fastapi uvicorn -jinja2 websockets diff --git a/web/monitor_routes.py b/web/monitor_routes.py index 43875488..ebad1460 100644 --- a/web/monitor_routes.py +++ b/web/monitor_routes.py @@ -1,4 +1,4 @@ -"""市场监控网页版 — 寄生 FastAPI,复用 _analyze() 全量数据。""" +"""市场监控网页版 — f-string 拼 HTML,零模板引擎依赖。""" from __future__ import annotations @@ -7,15 +7,13 @@ from typing import Any, Dict, List, Optional from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse -from fastapi.templating import Jinja2Templates from loguru import logger from web.analysis_service import _analyze router = APIRouter() -templates = Jinja2Templates(directory="web/templates") -# ── city config (与 telegram_push 一致) ── +# ── city config ── _CITIES: List[Dict[str, Any]] = [ {"key": "seoul", "en_name": "Seoul", "icao": "RKSI", "airport": "Incheon", "tz": 9, "tz_abbr": "KST", "rw": True}, @@ -34,147 +32,213 @@ _CITIES: List[Dict[str, Any]] = [ # ── helpers ── def _sf(v: Any) -> Optional[float]: - """Safe float.""" - if v is None: - return None - try: - return round(float(v), 1) - except (ValueError, TypeError): - return None + if v is None: return None + try: return round(float(v), 1) + except: return None -def _trend_info(icao: str) -> tuple[str, str]: - """Return (symbol, css_class) from _check_rising_trend.""" +def _trend_info(icao: str) -> tuple: try: from src.utils.telegram_push import _check_rising_trend - ok = _check_rising_trend(icao) - except Exception: - return ("→", "flat") - if ok: - return ("↑", "rising") - # Check if falling (temp decreasing) + if _check_rising_trend(icao): return ("↑", "rising") + except: pass try: from src.database.db_manager import DBManager obs = DBManager().get_airport_obs_recent(icao, minutes=60) temps = [r.get("temp_c") for r in obs if r.get("temp_c") is not None] if len(temps) >= 4 and temps[-1] < temps[len(temps)//2]: return ("↓", "falling") - except Exception: - pass + except: pass return ("→", "flat") def _obs_age(obs_time_str: Optional[str]) -> Optional[int]: - """Compute minutes since observation time.""" - if not obs_time_str: - return None + if not obs_time_str: return None try: - # Try parsing various formats - for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f", - "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S%z"): + for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d %H:%M:%S"): try: - dt = datetime.strptime(str(obs_time_str)[:26], fmt) - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - age = (datetime.now(timezone.utc) - dt).total_seconds() - return max(0, int(age // 60)) - except ValueError: - continue - # Try as epoch + dt = datetime.strptime(str(obs_time_str)[:26], fmt).replace(tzinfo=timezone.utc) + return max(0, int((datetime.now(timezone.utc) - dt).total_seconds() // 60)) + except: continue ts = float(obs_time_str) if ts > 1_000_000_000: dt = datetime.fromtimestamp(ts, tz=timezone.utc) - age = (datetime.now(timezone.utc) - dt).total_seconds() - return max(0, int(age // 60)) - except (ValueError, TypeError): - pass + return max(0, int((datetime.now(timezone.utc) - dt).total_seconds() // 60)) + except: pass return None -def _runway_pairs(city_weather: Dict[str, Any]) -> list: - """Extract runway pairs from AMOS data.""" - amos = city_weather.get("amos") or {} - rw_obs = (amos.get("runway_obs") or {}) if amos else {} - pairs = rw_obs.get("runway_pairs") or [] - temps = rw_obs.get("temperatures") or [] - result = [] - for (r1, r2), (t, _d) in zip(pairs, temps): - if t is not None: - result.append((f"{r1}/{r2}", round(float(t), 1))) - return result - -def _build_city_card(city: str, city_weather: Dict[str, Any], cfg: Dict[str, Any]) -> Dict[str, Any]: - """Build a single city's card data.""" - ac = city_weather.get("airport_current") or {} - cur = city_weather.get("current") or {} - ct = _sf(ac.get("temp")) or _sf(cur.get("temp")) - max_so_far = ac.get("max_so_far") - max_temp_time = ac.get("max_temp_time") - obs_time_str = ac.get("obs_time") or "" - local_time = city_weather.get("local_time") or "" - new_high = (ct is not None and max_so_far is not None and ct >= max_so_far + 0.3) - - trend_sym, trend_css = _trend_info(cfg["icao"]) - age = _obs_age(obs_time_str) - rw = _runway_pairs(city_weather) if cfg.get("rw") else [] - - return { - "en_name": cfg["en_name"], - "airport": cfg["airport"], - "icao": cfg["icao"], - "obs_time_str": obs_time_str or local_time, - "local_time": local_time, - "current_temp": ct, - "max_so_far": _sf(max_so_far), - "max_temp_time": max_temp_time, - "trend_sym": trend_sym, - "trend_css": trend_css, - "obs_age_min": age, - "new_high": new_high, - "runway_pairs": rw, - } - -def _load_all_cities() -> List[Dict[str, Any]]: +def _build_cards() -> tuple: cards = [] for cfg in _CITIES: try: cw = _analyze(cfg["key"]) - card = _build_city_card(cfg["key"], cw, cfg) - cards.append(card) - except Exception: - logger.exception("monitor: failed to load city {}", cfg["key"]) - # Sort by temp descending, None at bottom - cards.sort(key=lambda c: (c["current_temp"] is not None, c["current_temp"] or -999), reverse=True) - return cards + ac = cw.get("airport_current") or {} + cur = cw.get("current") or {} + ct = _sf(ac.get("temp")) or _sf(cur.get("temp")) + msf = ac.get("max_so_far") + mtt = ac.get("max_temp_time") or "" + obs = ac.get("obs_time") or "" + local_time = cw.get("local_time") or "" + new_high = (ct is not None and msf is not None and ct >= msf + 0.3) + tsym, tcss = _trend_info(cfg["icao"]) + age = _obs_age(obs) + + rw_html = "" + if cfg.get("rw"): + amos = cw.get("amos") or {} + rw_obs = (amos.get("runway_obs") or {}) if amos else {} + pairs = rw_obs.get("runway_pairs") or [] + temps = rw_obs.get("temperatures") or [] + for (r1, r2), (t, _d) in zip(pairs, temps): + if t is not None: + rw_html += f'