Ahmed Darwish commited on
Commit
b005543
·
0 Parent(s):

Static demo

Browse files
Files changed (2) hide show
  1. README.md +32 -0
  2. index.html +986 -0
README.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: ADS-B Flight Tracker
3
+ emoji: ✈️
4
+ colorFrom: green
5
+ colorTo: gray
6
+ sdk: static
7
+ pinned: false
8
+ license: mit
9
+ tags:
10
+ - iot
11
+ - raspberry-pi
12
+ - rtl-sdr
13
+ - ads-b
14
+ - flight-tracking
15
+ - demo
16
+ - arabic
17
+ ---
18
+
19
+ # ✈️ ADS-B Flight Tracker — Static Demo
20
+
21
+ This Space is a **free, client-side-only demo** of the radar dashboard — the exact same
22
+ `index.html` (Leaflet map, plane icons, trails, compass, retro ATC styling) as the real
23
+ project, running a JavaScript port of the simulation engine with no backend at all.
24
+
25
+ **What this demo can't show:** live traffic from real RTL-SDR hardware (Hugging Face Spaces
26
+ have no USB port), and the AviationStack flight-enrichment panel (needs a server to keep the
27
+ API key private). Both work fully in the real project.
28
+
29
+ 🔗 **Full project (Flask backend, LIVE hardware mode, systemd auto-start, Docker):**
30
+ [github.com/eahmeddarwish/adsb-flight-tracker](https://github.com/eahmeddarwish/adsb-flight-tracker)
31
+
32
+ Built by **Ahmed Darwish** — [eahmeddarwish@gmail.com](mailto:eahmeddarwish@gmail.com)
index.html ADDED
@@ -0,0 +1,986 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ADS-B RADAR — Live Demo (Simulation)</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Rajdhani:wght@300;400;600;700&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
10
+ <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
11
+ <style>
12
+ :root {
13
+ --radar-green: #00ff88;
14
+ --radar-dim: #00aa55;
15
+ --radar-glow: rgba(0,255,136,0.15);
16
+ --bg-deep: #030d06;
17
+ --bg-panel: #060f08;
18
+ --bg-card: #0a1a0d;
19
+ --border: rgba(0,255,136,0.2);
20
+ --border-bright: rgba(0,255,136,0.5);
21
+ --alert-red: #ff3355;
22
+ --alert-yellow: #ffcc00;
23
+ --text-dim: rgba(0,255,136,0.5);
24
+ }
25
+
26
+ * { margin: 0; padding: 0; box-sizing: border-box; }
27
+
28
+ body {
29
+ background: var(--bg-deep);
30
+ color: var(--radar-green);
31
+ font-family: 'Share Tech Mono', monospace;
32
+ height: 100vh;
33
+ overflow: hidden;
34
+ display: flex;
35
+ flex-direction: column;
36
+ }
37
+
38
+ /* ── SCANLINE OVERLAY ── */
39
+ body::before {
40
+ content: '';
41
+ position: fixed;
42
+ inset: 0;
43
+ background: repeating-linear-gradient(
44
+ 0deg,
45
+ transparent,
46
+ transparent 2px,
47
+ rgba(0,0,0,0.08) 2px,
48
+ rgba(0,0,0,0.08) 4px
49
+ );
50
+ pointer-events: none;
51
+ z-index: 9999;
52
+ }
53
+
54
+ /* ── HEADER ── */
55
+ header {
56
+ display: flex;
57
+ align-items: center;
58
+ justify-content: space-between;
59
+ padding: 8px 20px;
60
+ border-bottom: 1px solid var(--border);
61
+ background: var(--bg-panel);
62
+ flex-shrink: 0;
63
+ }
64
+
65
+ .header-left { display: flex; align-items: center; gap: 16px; }
66
+
67
+ .logo {
68
+ font-family: 'Rajdhani', sans-serif;
69
+ font-size: 22px;
70
+ font-weight: 700;
71
+ letter-spacing: 4px;
72
+ text-shadow: 0 0 20px var(--radar-green);
73
+ }
74
+
75
+ .logo span { color: var(--text-dim); font-weight: 300; }
76
+
77
+ .status-pill {
78
+ display: flex; align-items: center; gap: 6px;
79
+ font-size: 11px; letter-spacing: 2px;
80
+ padding: 3px 10px;
81
+ border: 1px solid var(--border);
82
+ background: var(--radar-glow);
83
+ }
84
+
85
+ .dot-live {
86
+ width: 7px; height: 7px; border-radius: 50%;
87
+ background: var(--radar-green);
88
+ box-shadow: 0 0 8px var(--radar-green);
89
+ animation: pulse-dot 1.2s ease-in-out infinite;
90
+ }
91
+
92
+ @keyframes pulse-dot {
93
+ 0%,100% { opacity: 1; }
94
+ 50% { opacity: 0.3; }
95
+ }
96
+
97
+ .header-stats {
98
+ display: flex; gap: 24px;
99
+ font-size: 11px; letter-spacing: 1px;
100
+ }
101
+
102
+ .hstat { display: flex; flex-direction: column; align-items: flex-end; }
103
+ .hstat-val { font-size: 20px; font-weight: 700; line-height: 1; text-shadow: 0 0 12px var(--radar-green); }
104
+ .hstat-label { color: var(--text-dim); font-size: 10px; letter-spacing: 2px; }
105
+
106
+ .utc-clock {
107
+ font-size: 24px;
108
+ letter-spacing: 3px;
109
+ text-shadow: 0 0 15px var(--radar-green);
110
+ border-left: 1px solid var(--border);
111
+ padding-left: 20px;
112
+ }
113
+
114
+ /* ── MAIN LAYOUT ── */
115
+ .main {
116
+ display: flex;
117
+ flex: 1;
118
+ overflow: hidden;
119
+ }
120
+
121
+ /* ── LEFT PANEL ── */
122
+ .left-panel {
123
+ width: 280px;
124
+ flex-shrink: 0;
125
+ background: var(--bg-panel);
126
+ border-right: 1px solid var(--border);
127
+ display: flex;
128
+ flex-direction: column;
129
+ overflow: hidden;
130
+ }
131
+
132
+ .panel-title {
133
+ padding: 10px 16px;
134
+ font-size: 10px;
135
+ letter-spacing: 3px;
136
+ color: var(--text-dim);
137
+ border-bottom: 1px solid var(--border);
138
+ display: flex;
139
+ justify-content: space-between;
140
+ align-items: center;
141
+ }
142
+
143
+ .panel-title span { color: var(--radar-green); }
144
+
145
+ /* flight list */
146
+ .flight-list {
147
+ flex: 1;
148
+ overflow-y: auto;
149
+ padding: 4px 0;
150
+ }
151
+
152
+ .flight-list::-webkit-scrollbar { width: 3px; }
153
+ .flight-list::-webkit-scrollbar-track { background: transparent; }
154
+ .flight-list::-webkit-scrollbar-thumb { background: var(--border); }
155
+
156
+ .flight-item {
157
+ padding: 8px 16px;
158
+ border-bottom: 1px solid rgba(0,255,136,0.05);
159
+ cursor: pointer;
160
+ transition: background 0.15s;
161
+ display: grid;
162
+ grid-template-columns: 1fr auto;
163
+ gap: 4px;
164
+ align-items: center;
165
+ }
166
+
167
+ .flight-item:hover { background: var(--radar-glow); }
168
+ .flight-item.selected { background: rgba(0,255,136,0.1); border-left: 2px solid var(--radar-green); }
169
+
170
+ .fi-callsign {
171
+ font-size: 14px;
172
+ font-weight: 700;
173
+ letter-spacing: 1px;
174
+ text-shadow: 0 0 8px var(--radar-green);
175
+ }
176
+
177
+ .fi-hex { font-size: 9px; color: var(--text-dim); letter-spacing: 1px; }
178
+
179
+ .fi-alt {
180
+ font-size: 12px;
181
+ text-align: right;
182
+ line-height: 1.2;
183
+ }
184
+
185
+ .fi-alt-val { color: var(--radar-green); }
186
+ .fi-speed { font-size: 10px; color: var(--text-dim); }
187
+
188
+ .fi-track {
189
+ grid-column: 1 / -1;
190
+ display: flex;
191
+ gap: 12px;
192
+ font-size: 9px;
193
+ color: var(--text-dim);
194
+ margin-top: 2px;
195
+ }
196
+
197
+ /* ── MAP AREA ── */
198
+ .map-wrapper {
199
+ flex: 1;
200
+ position: relative;
201
+ overflow: hidden;
202
+ }
203
+
204
+ #map {
205
+ width: 100%;
206
+ height: 100%;
207
+ background: #020a04 !important;
208
+ }
209
+
210
+ /* Custom Leaflet dark tiles override */
211
+ .leaflet-container { background: #020a04; }
212
+
213
+ /* MAP OVERLAY ELEMENTS */
214
+ .map-overlay-tl, .map-overlay-tr, .map-overlay-bl, .map-overlay-br {
215
+ position: absolute;
216
+ z-index: 1000;
217
+ pointer-events: none;
218
+ font-size: 9px;
219
+ letter-spacing: 2px;
220
+ color: var(--text-dim);
221
+ }
222
+ .map-overlay-tl { top: 12px; left: 12px; }
223
+ .map-overlay-tr { top: 12px; right: 12px; text-align: right; }
224
+ .map-overlay-bl { bottom: 30px; left: 12px; }
225
+ .map-overlay-br { bottom: 30px; right: 12px; text-align: right; }
226
+
227
+ /* SWEEP ANIMATION */
228
+ .radar-sweep {
229
+ position: absolute;
230
+ top: 50%; left: 50%;
231
+ width: 1px; height: 1px;
232
+ z-index: 999;
233
+ pointer-events: none;
234
+ display: none; /* hidden for map mode */
235
+ }
236
+
237
+ /* ── RIGHT PANEL ── */
238
+ .right-panel {
239
+ width: 260px;
240
+ flex-shrink: 0;
241
+ background: var(--bg-panel);
242
+ border-left: 1px solid var(--border);
243
+ display: flex;
244
+ flex-direction: column;
245
+ overflow: hidden;
246
+ }
247
+
248
+ /* SELECTED AIRCRAFT DETAIL */
249
+ .ac-detail {
250
+ padding: 14px 16px;
251
+ border-bottom: 1px solid var(--border);
252
+ flex-shrink: 0;
253
+ }
254
+
255
+ .ac-detail-callsign {
256
+ font-family: 'Rajdhani', sans-serif;
257
+ font-size: 28px;
258
+ font-weight: 700;
259
+ letter-spacing: 3px;
260
+ text-shadow: 0 0 20px var(--radar-green);
261
+ line-height: 1;
262
+ margin-bottom: 4px;
263
+ }
264
+
265
+ .ac-detail-hex {
266
+ font-size: 10px;
267
+ color: var(--text-dim);
268
+ letter-spacing: 2px;
269
+ margin-bottom: 12px;
270
+ }
271
+
272
+ .data-grid {
273
+ display: grid;
274
+ grid-template-columns: 1fr 1fr;
275
+ gap: 8px;
276
+ }
277
+
278
+ .data-cell {
279
+ background: var(--bg-card);
280
+ border: 1px solid var(--border);
281
+ padding: 8px 10px;
282
+ }
283
+
284
+ .dc-label {
285
+ font-size: 8px;
286
+ color: var(--text-dim);
287
+ letter-spacing: 2px;
288
+ margin-bottom: 3px;
289
+ }
290
+
291
+ .dc-value {
292
+ font-size: 16px;
293
+ font-weight: 700;
294
+ text-shadow: 0 0 8px var(--radar-green);
295
+ line-height: 1;
296
+ }
297
+
298
+ .dc-unit { font-size: 10px; color: var(--text-dim); }
299
+
300
+ /* HEADING COMPASS */
301
+ .compass-wrap {
302
+ display: flex;
303
+ flex-direction: column;
304
+ align-items: center;
305
+ padding: 14px 16px;
306
+ border-bottom: 1px solid var(--border);
307
+ flex-shrink: 0;
308
+ }
309
+
310
+ .compass-label {
311
+ font-size: 9px;
312
+ letter-spacing: 3px;
313
+ color: var(--text-dim);
314
+ margin-bottom: 8px;
315
+ }
316
+
317
+ .compass {
318
+ width: 90px; height: 90px;
319
+ position: relative;
320
+ }
321
+
322
+ .compass svg { width: 100%; height: 100%; }
323
+
324
+ /* LOG */
325
+ .log-section { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
326
+
327
+ .log-entries {
328
+ flex: 1;
329
+ overflow-y: auto;
330
+ padding: 6px 0;
331
+ }
332
+
333
+ .log-entries::-webkit-scrollbar { width: 3px; }
334
+ .log-entries::-webkit-scrollbar-thumb { background: var(--border); }
335
+
336
+ .log-entry {
337
+ padding: 5px 16px;
338
+ font-size: 10px;
339
+ border-bottom: 1px solid rgba(0,255,136,0.04);
340
+ display: flex;
341
+ gap: 8px;
342
+ color: var(--text-dim);
343
+ }
344
+
345
+ .log-time { color: var(--radar-green); flex-shrink: 0; }
346
+ .log-msg { flex: 1; }
347
+ .log-entry.alert { color: var(--alert-yellow); }
348
+ .log-entry.alert .log-time { color: var(--alert-yellow); }
349
+
350
+ /* FOOTER */
351
+ footer {
352
+ padding: 5px 20px;
353
+ border-top: 1px solid var(--border);
354
+ background: var(--bg-panel);
355
+ display: flex;
356
+ justify-content: space-between;
357
+ font-size: 9px;
358
+ color: var(--text-dim);
359
+ letter-spacing: 2px;
360
+ flex-shrink: 0;
361
+ }
362
+
363
+ /* LEAFLET CUSTOM STYLES */
364
+ .leaflet-tile-pane { filter: invert(1) hue-rotate(180deg) brightness(0.65) saturate(0.6) contrast(0.9); }
365
+ .leaflet-popup-content-wrapper {
366
+ background: var(--bg-panel);
367
+ border: 1px solid var(--border-bright);
368
+ border-radius: 0;
369
+ color: var(--radar-green);
370
+ font-family: 'Share Tech Mono', monospace;
371
+ font-size: 11px;
372
+ box-shadow: 0 0 20px var(--radar-glow);
373
+ }
374
+ .leaflet-popup-tip { background: var(--border-bright); }
375
+ .leaflet-popup-close-button { color: var(--radar-green) !important; }
376
+
377
+ /* No-selection state */
378
+ .no-selection {
379
+ padding: 20px 16px;
380
+ font-size: 10px;
381
+ color: var(--text-dim);
382
+ letter-spacing: 1px;
383
+ line-height: 1.8;
384
+ text-align: center;
385
+ }
386
+
387
+ .no-selection::before {
388
+ content: '[ SELECT AIRCRAFT ]';
389
+ display: block;
390
+ font-size: 9px;
391
+ letter-spacing: 3px;
392
+ margin-bottom: 12px;
393
+ color: rgba(0,255,136,0.3);
394
+ }
395
+
396
+ /* data source toggle */
397
+ .source-toggle {
398
+ display: flex;
399
+ gap: 0;
400
+ border: 1px solid var(--border);
401
+ overflow: hidden;
402
+ pointer-events: all;
403
+ }
404
+ .src-btn {
405
+ padding: 4px 12px;
406
+ font-size: 9px;
407
+ letter-spacing: 2px;
408
+ background: transparent;
409
+ color: var(--text-dim);
410
+ border: none;
411
+ cursor: pointer;
412
+ font-family: 'Share Tech Mono', monospace;
413
+ transition: all 0.15s;
414
+ }
415
+ .src-btn.active {
416
+ background: var(--radar-glow);
417
+ color: var(--radar-green);
418
+ }
419
+ .src-btn:hover { color: var(--radar-green); }
420
+ </style>
421
+ </head>
422
+ <body>
423
+
424
+ <!-- HEADER -->
425
+ <header>
426
+ <div class="header-left">
427
+ <div class="logo">ADS-B <span>RADAR</span></div>
428
+ <div class="status-pill">
429
+ <div class="dot-live"></div>
430
+ <span id="src-label">SIMULATION (static demo)</span>
431
+ </div>
432
+ <div class="source-toggle">
433
+ <button class="src-btn active" data-src="sim" onclick="setSource('sim')">SIM</button>
434
+ <button class="src-btn" data-src="live" onclick="setSource('live')">LIVE</button>
435
+ </div>
436
+ </div>
437
+
438
+ <div class="header-stats">
439
+ <div class="hstat">
440
+ <div class="hstat-val" id="stat-count">0</div>
441
+ <div class="hstat-label">AIRCRAFT</div>
442
+ </div>
443
+ <div class="hstat">
444
+ <div class="hstat-val" id="stat-msgs">0</div>
445
+ <div class="hstat-label">UPDATES/S</div>
446
+ </div>
447
+ <div class="hstat">
448
+ <div class="hstat-val" id="stat-range">—</div>
449
+ <div class="hstat-label">MAX ALT (ft)</div>
450
+ </div>
451
+ </div>
452
+
453
+ <div class="utc-clock" id="utc-clock">00:00:00Z</div>
454
+ </header>
455
+
456
+ <!-- MAIN -->
457
+ <div class="main">
458
+
459
+ <!-- LEFT: FLIGHT LIST -->
460
+ <div class="left-panel">
461
+ <div class="panel-title">
462
+ TRACKED FLIGHTS &nbsp;
463
+ <span id="list-count">0</span>
464
+ </div>
465
+ <div class="flight-list" id="flight-list">
466
+ <!-- populated by JS -->
467
+ </div>
468
+ </div>
469
+
470
+ <!-- MAP -->
471
+ <div class="map-wrapper">
472
+ <div id="map"></div>
473
+
474
+ <div class="map-overlay-tl">
475
+ KUWAIT ACC<br>
476
+ FIR: OKAC<br>
477
+ FREQ: 1090 MHz
478
+ </div>
479
+
480
+ <div class="map-overlay-tr" id="overlay-tr">
481
+ MODE: <span style="color:var(--radar-green)">SURVEILLANCE</span><br>
482
+ RANGE: 350 NM<br>
483
+ FILTER: ALL
484
+ </div>
485
+
486
+ <div class="map-overlay-bl" id="overlay-bl">
487
+ LAT: 29.37°N &nbsp; LON: 47.98°E
488
+ </div>
489
+
490
+ <div class="map-overlay-br">
491
+ © ADS-B TRACKER — KCST FYP<br>
492
+ POWERED BY RTL-SDR + DUMP1090
493
+ </div>
494
+ </div>
495
+
496
+ <!-- RIGHT: DETAIL PANEL -->
497
+ <div class="right-panel">
498
+ <div class="panel-title">AIRCRAFT DETAIL</div>
499
+
500
+ <div id="ac-detail-area">
501
+ <div class="no-selection">
502
+ Click any aircraft on the map or in the list to view detailed telemetry
503
+ </div>
504
+ </div>
505
+
506
+ <div class="compass-wrap" id="compass-wrap" style="display:none">
507
+ <div class="compass-label">HEADING / TRACK</div>
508
+ <div class="compass">
509
+ <svg viewBox="0 0 90 90">
510
+ <!-- Outer ring -->
511
+ <circle cx="45" cy="45" r="42" fill="none" stroke="rgba(0,255,136,0.2)" stroke-width="1"/>
512
+ <circle cx="45" cy="45" r="38" fill="none" stroke="rgba(0,255,136,0.08)" stroke-width="1"/>
513
+ <!-- Cardinal ticks -->
514
+ <line x1="45" y1="3" x2="45" y2="12" stroke="rgba(0,255,136,0.6)" stroke-width="1.5"/>
515
+ <line x1="87" y1="45" x2="78" y2="45" stroke="rgba(0,255,136,0.3)" stroke-width="1"/>
516
+ <line x1="45" y1="87" x2="45" y2="78" stroke="rgba(0,255,136,0.3)" stroke-width="1"/>
517
+ <line x1="3" y1="45" x2="12" y2="45" stroke="rgba(0,255,136,0.3)" stroke-width="1"/>
518
+ <!-- Labels -->
519
+ <text x="45" y="20" text-anchor="middle" fill="rgba(0,255,136,0.8)" font-family="Share Tech Mono" font-size="7">N</text>
520
+ <text x="72" y="49" text-anchor="middle" fill="rgba(0,255,136,0.4)" font-family="Share Tech Mono" font-size="6">E</text>
521
+ <text x="45" y="75" text-anchor="middle" fill="rgba(0,255,136,0.4)" font-family="Share Tech Mono" font-size="6">S</text>
522
+ <text x="18" y="49" text-anchor="middle" fill="rgba(0,255,136,0.4)" font-family="Share Tech Mono" font-size="6">W</text>
523
+ <!-- Needle -->
524
+ <line id="compass-needle"
525
+ x1="45" y1="45" x2="45" y2="10"
526
+ stroke="#00ff88" stroke-width="2"
527
+ stroke-linecap="round"
528
+ transform-origin="45 45"
529
+ style="filter: drop-shadow(0 0 4px #00ff88)"
530
+ />
531
+ <circle cx="45" cy="45" r="3" fill="#00ff88" style="filter: drop-shadow(0 0 6px #00ff88)"/>
532
+ </svg>
533
+ </div>
534
+ </div>
535
+
536
+ <div class="log-section">
537
+ <div class="panel-title">SYSTEM LOG <span id="log-count">0</span></div>
538
+ <div class="log-entries" id="log-entries"></div>
539
+ </div>
540
+ </div>
541
+ </div>
542
+
543
+ <!-- FOOTER -->
544
+ <footer>
545
+ <span>STATIC DEMO &nbsp;|&nbsp; <a href="https://github.com/eahmeddarwish/adsb-flight-tracker" target="_blank" style="color:var(--radar-green)">Full project + LIVE mode on GitHub</a></span>
546
+ <span id="footer-time">LAST UPDATE: —</span>
547
+ <span>KCST · FYP · ADS-B TRACKER v2.0</span>
548
+ </footer>
549
+
550
+ <!-- ───────────────────────── SCRIPTS ───────────────────────── -->
551
+ <script>
552
+
553
+ // ── CONFIG ──────────────────��───────────────────────────────
554
+ const CONFIG = {
555
+ // Static demo build: no backend at all. Aircraft are generated entirely
556
+ // client-side (see SIM_ENGINE below). The real project (GitHub) runs a
557
+ // Flask server that also supports real RTL-SDR hardware in LIVE mode.
558
+ center: [29.37, 47.98],
559
+ zoom: 7,
560
+ refresh: 1000,
561
+ repo_url: 'https://github.com/eahmeddarwish/adsb-flight-tracker'
562
+ };
563
+
564
+ // ── CLIENT-SIDE SIMULATION ENGINE ─────────────────────────────
565
+ // A JS port of app/simulate.py's motion model, so this static build needs
566
+ // no server at all. Same math, same look, just running in the browser.
567
+ const SIM_ENGINE = (() => {
568
+ let planes = [
569
+ {hex:'A1B2C3', flight:'KAC123', lat:29.37, lon:47.98, alt:32000, speed:430, track:120},
570
+ {hex:'D4E5F6', flight:'UAE201', lat:29.45, lon:48.10, alt:36000, speed:460, track:300},
571
+ {hex:'G7H8I9', flight:'QTR900', lat:29.20, lon:47.80, alt:38000, speed:480, track:45},
572
+ ];
573
+ function step() {
574
+ for (const p of planes) {
575
+ const dir = p.track * Math.PI / 180;
576
+ p.lat += 0.01 * Math.cos(dir);
577
+ p.lon += 0.01 * Math.sin(dir);
578
+ if (p.lat < 24.0 || p.lat > 34.0) p.track = (p.track + 180) % 360;
579
+ if (p.lon < 43.0 || p.lon > 53.0) p.track = (p.track + 180) % 360;
580
+ }
581
+ return planes;
582
+ }
583
+ return { step };
584
+ })();
585
+
586
+ let currentSource = 'sim';
587
+ let selectedHex = null;
588
+ let planeMarkers = {};
589
+ let planePaths = {};
590
+ let planeData = {};
591
+ let updateCount = 0;
592
+ let logCount = 0;
593
+
594
+ // ── MAP INIT ────────────────────────────────────────────────
595
+ const map = L.map('map', {
596
+ zoomControl: true,
597
+ attributionControl: false
598
+ }).setView(CONFIG.center, CONFIG.zoom);
599
+
600
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
601
+
602
+ // update coords overlay on mouse move
603
+ map.on('mousemove', e => {
604
+ document.getElementById('overlay-bl').innerHTML =
605
+ `LAT: ${e.latlng.lat.toFixed(4)}°N &nbsp; LON: ${e.latlng.lng.toFixed(4)}°E`;
606
+ });
607
+
608
+ // ── PLANE SVG ICON (real aircraft top-down shape) ────────────
609
+ function makePlaneIcon(track, isSelected) {
610
+ const color = isSelected ? '#ffffff' : '#00ff88';
611
+ const glow = isSelected ? '0 0 10px #fff, 0 0 20px #fff' : '0 0 8px #00ff88, 0 0 16px #00aa55';
612
+ const shadow = isSelected ? 'drop-shadow(0 0 5px #fff)' : 'drop-shadow(0 0 4px #00ff88)';
613
+ // Real top-down aircraft path from plane.svg (scaled to 44x44 viewBox)
614
+ const planePath = `M2504 5083 c-69 -68 -128 -238 -159 -453 -15 -107 -34 -457 -38 -720
615
+ l-2 -144 -265 -223 -265 -223 -5 130 c-4 101 -8 133 -20 140 -22 14 -184 12
616
+ -198 -2 -9 -9 -12 -76 -12 -239 l0 -226 -232 -195 -233 -196 -3 129 c-3 129
617
+ -3 130 -28 139 -33 13 -145 13 -178 0 l-26 -10 0 -227 0 -228 -358 -301 c-196
618
+ -165 -367 -315 -380 -334 -40 -59 -54 -127 -59 -270 l-4 -135 29 32 c22 26
619
+ 113 70 432 212 223 99 411 181 419 183 8 2 17 -10 23 -29 11 -40 35 -43 44 -5
620
+ 16 73 17 75 115 119 52 23 97 42 102 42 4 1 7 -8 7 -18 0 -62 35 -68 50 -8 6
621
+ 23 12 47 14 52 3 8 118 63 191 91 7 3 17 -12 24 -36 17 -58 38 -51 51 18 l12
622
+ 59 366 162 c202 90 372 165 379 168 8 3 12 -143 16 -584 5 -619 11 -714 56
623
+ -876 l19 -68 -317 -262 c-174 -143 -322 -268 -329 -277 -76 -89 -134 -226
624
+ -148 -348 -11 -98 -18 -99 454 81 389 148 407 154 414 133 3 -11 18 -77 33
625
+ -146 54 -252 76 -252 130 0 15 69 30 135 33 146 7 21 25 15 414 -133 472 -180
626
+ 465 -179 454 -81 -14 122 -72 259 -148 348 -7 9 -155 134 -329 277 l-317 262
627
+ 19 68 c45 162 51 257 56 876 4 441 8 587 16 584 7 -3 177 -78 379 -168 l366
628
+ -162 12 -59 c13 -69 34 -76 51 -18 7 24 17 39 24 36 73 -28 188 -83 191 -91 2
629
+ -5 8 -29 14 -52 15 -60 50 -54 50 8 0 10 3 19 8 18 4 0 49 -19 101 -42 98 -44
630
+ 99 -46 115 -119 9 -38 33 -35 44 5 6 19 15 31 23 29 8 -2 196 -84 419 -183
631
+ 319 -142 410 -186 432 -212 l29 -32 -4 135 c-5 139 -18 205 -56 267 -11 17
632
+ -182 168 -380 335 l-361 303 0 227 c0 214 -1 228 -19 238 -25 13 -151 13 -185
633
+ 0 -25 -9 -25 -10 -28 -139 l-3 -129 -232 196 -233 195 0 226 c0 163 -3 230
634
+ -12 239 -14 14 -176 16 -198 2 -12 -7 -16 -39 -20 -140 l-5 -130 -265 223
635
+ -265 223 -2 144 c-4 263 -23 613 -38 720 -20 138 -58 280 -96 358 -35 71 -90
636
+ 132 -119 132 -11 0 -36 -16 -56 -37z`;
637
+
638
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg"
639
+ viewBox="0 0 512 512" width="42" height="42"
640
+ style="transform:rotate(${track}deg); filter:${shadow}; transform-origin:center;">
641
+ <g transform="translate(0,512) scale(0.1,-0.1)">
642
+ <path d="${planePath}" fill="${color}" fill-opacity="0.95"/>
643
+ </g>
644
+ </svg>`;
645
+
646
+ return L.divIcon({
647
+ html: svg,
648
+ iconSize: [42, 42],
649
+ iconAnchor: [21, 21],
650
+ className: ''
651
+ });
652
+ }
653
+
654
+ // ── PARSE AIRCRAFT (handles both dump1090 and sim formats) ───
655
+ function parseAircraft(raw) {
656
+ return {
657
+ hex: (raw.hex || raw.icao || '??????').toUpperCase(),
658
+ flight: (raw.flight || raw.callsign || '').trim() || '——',
659
+ lat: raw.lat ?? raw.latitude ?? null,
660
+ lon: raw.lon ?? raw.longitude ?? null,
661
+ alt: raw.alt ?? raw.alt_baro ?? raw.altitude ?? 0, // always numeric, never undefined
662
+ speed: raw.speed ?? raw.gs ?? 0,
663
+ track: raw.track ?? raw.true_heading ?? 0,
664
+ };
665
+ }
666
+
667
+ // ── UPDATE MARKERS ───────────────────────────────────────────
668
+ function updateMap(aircraft) {
669
+ const seen = new Set();
670
+
671
+ aircraft.forEach(raw => {
672
+ const ac = parseAircraft(raw);
673
+ if (ac.lat === null || ac.lon === null) return;
674
+ seen.add(ac.hex);
675
+ planeData[ac.hex] = ac;
676
+
677
+ if (planeMarkers[ac.hex]) {
678
+ planeMarkers[ac.hex].setLatLng([ac.lat, ac.lon]);
679
+ planeMarkers[ac.hex].setIcon(makePlaneIcon(ac.track, ac.hex === selectedHex));
680
+ } else {
681
+ const marker = L.marker([ac.lat, ac.lon], {
682
+ icon: makePlaneIcon(ac.track, false),
683
+ zIndexOffset: 1000
684
+ }).addTo(map);
685
+
686
+ marker.on('click', () => selectAircraft(ac.hex));
687
+ planeMarkers[ac.hex] = marker;
688
+ planePaths[ac.hex] = [];
689
+ addLog(`CONTACT: ${ac.flight} [${ac.hex}] ALT:${ac.alt}ft`);
690
+ }
691
+
692
+ // trail dots
693
+ planePaths[ac.hex].push([ac.lat, ac.lon]);
694
+ if (planePaths[ac.hex].length > 80) planePaths[ac.hex].shift();
695
+
696
+ // draw trail — two layers: glow + solid line
697
+ if (planeMarkers[ac.hex]._trail) map.removeLayer(planeMarkers[ac.hex]._trail);
698
+ if (planeMarkers[ac.hex]._trailGlow) map.removeLayer(planeMarkers[ac.hex]._trailGlow);
699
+
700
+ if (planePaths[ac.hex].length > 1) {
701
+ const isSelected = ac.hex === selectedHex;
702
+ const trailColor = isSelected ? 'rgba(255,255,255,0.85)' : 'rgba(0,255,136,0.75)';
703
+ const glowColor = isSelected ? 'rgba(255,255,255,0.2)' : 'rgba(0,255,136,0.2)';
704
+
705
+ // glow layer (thick, transparent)
706
+ planeMarkers[ac.hex]._trailGlow = L.polyline(planePaths[ac.hex], {
707
+ color: glowColor,
708
+ weight: 6,
709
+ lineCap: 'round'
710
+ }).addTo(map);
711
+
712
+ // solid line on top
713
+ planeMarkers[ac.hex]._trail = L.polyline(planePaths[ac.hex], {
714
+ color: trailColor,
715
+ weight: 2,
716
+ dashArray: '6 5',
717
+ lineCap: 'round'
718
+ }).addTo(map);
719
+ }
720
+
721
+ // popup
722
+ planeMarkers[ac.hex].bindPopup(
723
+ `<b style="font-size:14px;letter-spacing:2px">${ac.flight}</b><br>` +
724
+ `HEX: ${ac.hex}<br>` +
725
+ `ALT: ${ac.alt.toLocaleString()} ft<br>` +
726
+ `SPD: ${ac.speed} kts<br>` +
727
+ `TRK: ${ac.track}°<br>` +
728
+ `POS: ${ac.lat.toFixed(4)}° / ${ac.lon.toFixed(4)}°`
729
+ );
730
+ });
731
+
732
+ // remove stale markers
733
+ Object.keys(planeMarkers).forEach(hex => {
734
+ if (!seen.has(hex)) {
735
+ map.removeLayer(planeMarkers[hex]);
736
+ if (planeMarkers[hex]._trail) map.removeLayer(planeMarkers[hex]._trail);
737
+ if (planeMarkers[hex]._trailGlow) map.removeLayer(planeMarkers[hex]._trailGlow);
738
+ delete planeMarkers[hex];
739
+ delete planePaths[hex];
740
+ delete planeData[hex];
741
+ }
742
+ });
743
+
744
+ updateFlightList(aircraft.map(parseAircraft).filter(a => a.lat !== null));
745
+ updateStats(aircraft);
746
+ updateSelectedDetail();
747
+ }
748
+
749
+ // ── FLIGHT LIST ──────────────────────────────────────────────
750
+ function updateFlightList(acs) {
751
+ const list = document.getElementById('flight-list');
752
+ document.getElementById('list-count').textContent = acs.length;
753
+
754
+ // sort by altitude desc
755
+ acs.sort((a, b) => b.alt - a.alt);
756
+
757
+ list.innerHTML = acs.map(ac => `
758
+ <div class="flight-item ${ac.hex === selectedHex ? 'selected' : ''}"
759
+ onclick="selectAircraft('${ac.hex}')">
760
+ <div>
761
+ <div class="fi-callsign">${ac.flight}</div>
762
+ <div class="fi-hex">${ac.hex}</div>
763
+ </div>
764
+ <div class="fi-alt">
765
+ <div class="fi-alt-val">${ac.alt.toLocaleString()}</div>
766
+ <div class="fi-speed">${ac.speed} kts</div>
767
+ </div>
768
+ <div class="fi-track">
769
+ <span>LAT ${ac.lat?.toFixed(3) ?? '—'}</span>
770
+ <span>LON ${ac.lon?.toFixed(3) ?? '—'}</span>
771
+ <span>TRK ${ac.track}°</span>
772
+ </div>
773
+ </div>
774
+ `).join('');
775
+ }
776
+
777
+ // ── AVIATIONSTACK — جلب تفاصيل الرحلة ───────────────────────
778
+ const flightCache = {};
779
+
780
+ async function fetchFlightDetails(callsign) {
781
+ // Static demo has no backend to proxy AviationStack through.
782
+ // Run the full project from GitHub to enable this panel.
783
+ return null;
784
+ }
785
+
786
+ // ── SELECT AIRCRAFT ──────────────────────────────────────────
787
+ function selectAircraft(hex) {
788
+ selectedHex = hex;
789
+ // refresh icons
790
+ Object.keys(planeMarkers).forEach(h => {
791
+ const ac = planeData[h];
792
+ if (ac) planeMarkers[h].setIcon(makePlaneIcon(ac.track, h === hex));
793
+ });
794
+ updateSelectedDetail();
795
+ // pan to
796
+ if (planeData[hex]) {
797
+ map.panTo([planeData[hex].lat, planeData[hex].lon]);
798
+ }
799
+ }
800
+
801
+ async function updateSelectedDetail() {
802
+ const area = document.getElementById('ac-detail-area');
803
+ const cw = document.getElementById('compass-wrap');
804
+
805
+ if (!selectedHex || !planeData[selectedHex]) {
806
+ area.innerHTML = `<div class="no-selection">Click any aircraft on the map or in the list to view detailed telemetry</div>`;
807
+ cw.style.display = 'none';
808
+ return;
809
+ }
810
+
811
+ const ac = planeData[selectedHex];
812
+ cw.style.display = 'flex';
813
+
814
+ // rotate compass needle
815
+ document.getElementById('compass-needle').setAttribute(
816
+ 'transform', `rotate(${ac.track}, 45, 45)`
817
+ );
818
+
819
+ // اعرض البيانات الأساسية أول
820
+ renderDetail(ac, null);
821
+
822
+ // بعدين جيب من AviationStack في الخلفية
823
+ const info = await fetchFlightDetails(ac.flight);
824
+ renderDetail(ac, info);
825
+ }
826
+
827
+ function renderDetail(ac, info) {
828
+ const area = document.getElementById('ac-detail-area');
829
+
830
+ // ── بيانات AviationStack ──
831
+ const airline = info?.airline?.name || '——';
832
+ const acType = info?.aircraft?.iata || info?.aircraft?.icao || '——';
833
+ const acReg = info?.aircraft?.registration || '——';
834
+ const depIata = info?.departure?.iata || '——';
835
+ const depName = info?.departure?.airport || '——';
836
+ const depSched = info?.departure?.scheduled ? info.departure.scheduled.slice(11,16) + 'Z' : '——';
837
+ const depActual = info?.departure?.actual ? info.departure.actual.slice(11,16) + 'Z' : '——';
838
+ const arrIata = info?.arrival?.iata || '——';
839
+ const arrName = info?.arrival?.airport || '——';
840
+ const arrSched = info?.arrival?.scheduled ? info.arrival.scheduled.slice(11,16) + 'Z' : '——';
841
+ const arrEst = info?.arrival?.estimated ? info.arrival.estimated.slice(11,16) + 'Z' : '——';
842
+ const status = info?.flight_status || '——';
843
+
844
+ const statusColor = status === 'active' ? 'var(--radar-green)'
845
+ : status === 'landed' ? 'var(--alert-yellow)'
846
+ : status === 'delayed' ? 'var(--alert-red)'
847
+ : 'var(--text-dim)';
848
+
849
+ area.innerHTML = `
850
+ <div class="ac-detail">
851
+
852
+ <!-- Callsign + Status -->
853
+ <div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:6px">
854
+ <div>
855
+ <div class="ac-detail-callsign">${ac.flight}</div>
856
+ <div class="ac-detail-hex">ICAO: ${ac.hex}</div>
857
+ </div>
858
+ <div style="text-align:right;font-size:10px;letter-spacing:1px">
859
+ <div style="color:${statusColor};text-transform:uppercase;font-size:11px">${status}</div>
860
+ <div style="color:var(--text-dim);margin-top:2px">${acType}</div>
861
+ <div style="color:var(--text-dim)">${acReg}</div>
862
+ </div>
863
+ </div>
864
+
865
+ <!-- Airline -->
866
+ <div style="font-size:11px;color:var(--radar-green);letter-spacing:1px;
867
+ border:1px solid var(--border);padding:5px 8px;margin-bottom:8px;
868
+ background:var(--radar-glow)">
869
+ ✈ ${airline}
870
+ </div>
871
+
872
+ <!-- FROM / TO -->
873
+ <div style="display:grid;grid-template-columns:1fr 8px 1fr;gap:4px;margin-bottom:8px;align-items:center">
874
+ <div style="background:var(--bg-card);border:1px solid var(--border);padding:7px 8px">
875
+ <div style="font-size:8px;color:var(--text-dim);letter-spacing:2px;margin-bottom:3px">FROM 🛫</div>
876
+ <div style="font-size:20px;font-weight:700;text-shadow:0 0 8px var(--radar-green)">${depIata}</div>
877
+ <div style="font-size:9px;color:var(--text-dim);margin-top:2px;line-height:1.3">${depName.slice(0,18)}</div>
878
+ <div style="font-size:9px;color:var(--radar-green);margin-top:3px">SCH ${depSched}</div>
879
+ <div style="font-size:9px;color:var(--text-dim)">ACT ${depActual}</div>
880
+ </div>
881
+ <div style="text-align:center;color:var(--text-dim);font-size:10px">›</div>
882
+ <div style="background:var(--bg-card);border:1px solid var(--border);padding:7px 8px">
883
+ <div style="font-size:8px;color:var(--text-dim);letter-spacing:2px;margin-bottom:3px">TO 🛬</div>
884
+ <div style="font-size:20px;font-weight:700;text-shadow:0 0 8px var(--radar-green)">${arrIata}</div>
885
+ <div style="font-size:9px;color:var(--text-dim);margin-top:2px;line-height:1.3">${arrName.slice(0,18)}</div>
886
+ <div style="font-size:9px;color:var(--radar-green);margin-top:3px">SCH ${arrSched}</div>
887
+ <div style="font-size:9px;color:var(--text-dim)">EST ${arrEst}</div>
888
+ </div>
889
+ </div>
890
+
891
+ <!-- Telemetry grid -->
892
+ <div class="data-grid">
893
+ <div class="data-cell">
894
+ <div class="dc-label">ALTITUDE</div>
895
+ <div class="dc-value">${ac.alt.toLocaleString()}<span class="dc-unit"> ft</span></div>
896
+ </div>
897
+ <div class="data-cell">
898
+ <div class="dc-label">SPEED</div>
899
+ <div class="dc-value">${ac.speed}<span class="dc-unit"> kts</span></div>
900
+ </div>
901
+ <div class="data-cell">
902
+ <div class="dc-label">LATITUDE</div>
903
+ <div class="dc-value" style="font-size:13px">${ac.lat?.toFixed(4) ?? '—'}°</div>
904
+ </div>
905
+ <div class="data-cell">
906
+ <div class="dc-label">LONGITUDE</div>
907
+ <div class="dc-value" style="font-size:13px">${ac.lon?.toFixed(4) ?? '—'}°</div>
908
+ </div>
909
+ <div class="data-cell" style="grid-column:1/-1">
910
+ <div class="dc-label">TRACK</div>
911
+ <div class="dc-value">${ac.track}<span class="dc-unit">°</span></div>
912
+ </div>
913
+ </div>
914
+ ${!info ? `<div style="font-size:9px;color:var(--text-dim);text-align:center;margin-top:6px;letter-spacing:1px">STATIC DEMO — <a href="${CONFIG.repo_url}" target="_blank" style="color:var(--radar-green)">run locally</a> for flight enrichment</div>` : ''}
915
+ </div>
916
+ `;
917
+ }
918
+
919
+ // ── STATS ────────────────────────────────────────────────────
920
+ function updateStats(aircraft) {
921
+ document.getElementById('stat-count').textContent = aircraft.length;
922
+ document.getElementById('stat-msgs').textContent = updateCount;
923
+ const maxAlt = aircraft.reduce((m, a) => Math.max(m, a.alt ?? 0), 0);
924
+ document.getElementById('stat-range').textContent = maxAlt ? maxAlt.toLocaleString() : '—';
925
+ updateCount = 0;
926
+
927
+ const now = new Date().toISOString().replace('T',' ').split('.')[0];
928
+ document.getElementById('footer-time').textContent = `LAST UPDATE: ${now}Z`;
929
+ }
930
+
931
+ // ── LOG ──────────────────────────────────────────────────────
932
+ function addLog(msg, type='') {
933
+ logCount++;
934
+ const el = document.getElementById('log-entries');
935
+ const count = document.getElementById('log-count');
936
+ const time = new Date().toTimeString().slice(0,8);
937
+ const entry = document.createElement('div');
938
+ entry.className = `log-entry ${type}`;
939
+ entry.innerHTML = `<span class="log-time">${time}Z</span><span class="log-msg">${msg}</span>`;
940
+ el.insertBefore(entry, el.firstChild);
941
+ if (el.children.length > 80) el.removeChild(el.lastChild);
942
+ count.textContent = logCount;
943
+ }
944
+
945
+ // ── CLOCK ────────────────────────────────────────────────────
946
+ function updateClock() {
947
+ const now = new Date();
948
+ const h = String(now.getUTCHours()).padStart(2,'0');
949
+ const m = String(now.getUTCMinutes()).padStart(2,'0');
950
+ const s = String(now.getUTCSeconds()).padStart(2,'0');
951
+ document.getElementById('utc-clock').textContent = `${h}:${m}:${s}Z`;
952
+ }
953
+ setInterval(updateClock, 1000);
954
+ updateClock();
955
+
956
+ // ── DATA SOURCE ──────────────────────────────────────────────
957
+ function setSource(src) {
958
+ if (src === 'live') {
959
+ addLog('LIVE mode needs real RTL-SDR hardware - clone the repo from GitHub to run it', 'alert');
960
+ window.open(CONFIG.repo_url, '_blank');
961
+ return; // stay on simulation - this static build has no backend to switch to
962
+ }
963
+ currentSource = src;
964
+ document.querySelectorAll('.src-btn').forEach(b => b.classList.toggle('active', b.dataset.src === src));
965
+ document.getElementById('src-label').textContent = 'SIMULATION (static demo)';
966
+ }
967
+
968
+ // ── FETCH LOOP ───────────────────────────────────────────────
969
+ function fetchAircraft() {
970
+ // No network at all in the static build - just advance the local simulation.
971
+ const list = SIM_ENGINE.step();
972
+ updateCount++;
973
+ updateMap(list);
974
+ }
975
+
976
+ // startup log
977
+ addLog('SYSTEM INITIALIZED — ADS-B TRACKER v2.0 (STATIC DEMO)');
978
+ addLog('Running client-side simulation — no server, no hardware');
979
+ addLog('Full project with real RTL-SDR support: see GitHub link below');
980
+
981
+ fetchAircraft();
982
+ setInterval(fetchAircraft, CONFIG.refresh);
983
+
984
+ </script>
985
+ </body>
986
+ </html>