"""Builds the neon 'hacker' sidebar: MOST TRACKED + AIRPORT DISRUPTIONS panels. Mirrors the FlightRadar24 sidebar layout. Since the FR24 API exposes neither viewer/tracker counts nor an airport-disruption feed, both panels are derived from the live in-view flights (and real weather from Open-Meteo). Labels make the derivation explicit so nothing is presented as official FR24 data. """ from __future__ import annotations from collections import Counter import fr24 import weather _AIRPORT_NAMES = { "EGLL": "London", "KJFK": "New York", "OMDB": "Dubai", "EGBB": "Birmingham", "ZGSZ": "Shenzhen", "KDFW": "Dallas", "LTBJ": "Izmir", "EHAM": "Amsterdam", "LFPG": "Paris", "EDDF": "Frankfurt", "RJTT": "Tokyo", "VHHH": "Hong Kong", } def _fmt_int(n) -> str: try: return f"{int(n):,}" except Exception: return "—" def _esc(s) -> str: return (str(s).replace("&", "&").replace("<", "<").replace(">", ">") if s is not None else "") def _most_tracked(flights, limit=8): ranked = sorted( [f for f in flights if f.get("gspeed")], key=lambda f: f.get("gspeed") or 0, reverse=True, )[:limit] rows = [] for i, f in enumerate(ranked, 1): cs = _esc(f.get("callsign") or f.get("flight") or "N/A") typ = _esc(f.get("type") or "—") orig = _esc(f.get("orig") or "N/A") dest = _esc(f.get("dest") or "N/A") rows.append(f"""
{i}. {cs} {typ} {_fmt_int(f.get('gspeed'))} kt
{orig} {dest}
""") if not rows: rows.append('
no contacts
') return "".join(rows) def _disruptions(flights, limit=6): # Busiest destinations = inbound congestion proxy. counts = Counter( f.get("dest") for f in flights if f.get("dest")) top = counts.most_common(limit) if not top: return '
no inbound traffic
' max_n = max(n for _, n in top) or 1 rows = [] for code, n in top: ll = fr24.airport_coords(code) wx = weather.current(*ll) if ll else None name = _AIRPORT_NAMES.get(code, (code or "—")) if wx: temp = (f"{round(wx['temp_c'])}°C" if wx.get("temp_c") is not None else "—°C") wind = (f"{weather.wind_arrow(wx.get('wind_dir'))} " f"{round(wx.get('wind_dir') or 0)}° " f"{round(wx.get('wind_kts') or 0)} kts") sky = weather.code_glyph(wx.get("code")) else: temp, wind, sky = "—", "wx n/a", "·" # Derived 0.0–9.9 congestion index (NOT FR24's official rating). idx = round(min(9.9, 9.9 * (n / max_n)), 1) dot = "#FF2BD6" if idx >= 6 else ("#FFD23F" if idx >= 3 else "#39FF14") rows.append(f"""
{_esc(name)} {_esc(code)}
{sky} {temp} {wind}
🛬 {n} ● {idx}
""") return "".join(rows) def build_sidebar_html(flights) -> str: tracked = _most_tracked(flights) disrupt = _disruptions(flights) return f"""
MOST TRACKED LIVE
ranked by ground speed · API has no viewer counts
{tracked}
AIRPORT DISRUPTIONS LIVE
busiest inbound · weather: open-meteo · index derived
{disrupt}
"""