File size: 4,444 Bytes
41e0c9e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
            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"""
          <div class="fd-item">
            <div class="fd-row">
              <span class="fd-rank">{i}.</span>
              <span class="fd-cs">{cs}</span>
              <span class="fd-badge">{typ}</span>
              <span class="fd-metric">{_fmt_int(f.get('gspeed'))} <small>kt</small></span>
            </div>
            <div class="fd-route">{orig} <span class="fd-arrow">–</span> {dest}</div>
          </div>""")
    if not rows:
        rows.append('<div class="fd-empty">no contacts</div>')
    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 '<div class="fd-empty">no inbound traffic</div>'

    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"""
          <div class="fd-item">
            <div class="fd-row">
              <span class="fd-cs">{_esc(name)}</span>
              <span class="fd-badge">{_esc(code)}</span>
            </div>
            <div class="fd-wx">
              <span>{sky} {temp}</span>
              <span class="fd-wind">{wind}</span>
            </div>
            <div class="fd-scores">
              <span title="inbound contacts in view">🛬 {n}</span>
              <span class="fd-dot" style="color:{dot}">● {idx}</span>
            </div>
          </div>""")
    return "".join(rows)


def build_sidebar_html(flights) -> str:
    tracked = _most_tracked(flights)
    disrupt = _disruptions(flights)
    return f"""
<div id="fd-sidebar">
  <details open class="fd-panel">
    <summary>MOST TRACKED <span class="fd-live">LIVE</span><span class="fd-chev">▾</span></summary>
    <div class="fd-note">ranked by ground speed · API has no viewer counts</div>
    <div class="fd-list">{tracked}</div>
  </details>

  <details open class="fd-panel">
    <summary>AIRPORT DISRUPTIONS <span class="fd-live">LIVE</span><span class="fd-chev">▾</span></summary>
    <div class="fd-note">busiest inbound · weather: open-meteo · index derived</div>
    <div class="fd-list">{disrupt}</div>
  </details>
</div>"""