swadhinbiswas

[email protected]
2026-09-12 11:15:48 +0600 committed NaN years ago
d7ba82e
commit d7ba82e
6 files changed
+79 -41
▼web/index.html
+9-3
2
2
<html lang="en" class="dark">
3
3
<head>
4
4
<meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
6
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
<meta
8
8
name="description"
9
- content="EU Air Traffic — live God's Eye View of European airspace plus Medallion analytics (Bronze → Silver → Gold) built with DuckDB, dbt and a live ADS-B feed."
9
+ content="EU Air Traffic — live European airspace with operational aircraft classification, carbon estimates and Gold-layer analytics on MotherDuck."
10
10
/>
11
11
<meta name="theme-color" content="#05070d" />
12
- <title>EU Air Traffic · God's Eye View</title>
12
+ <meta property="og:title" content="EU Air Traffic" />
13
+ <meta
14
+ property="og:description"
15
+ content="Live European airspace intelligence: cargo, military and civil traffic with weather, carbon and network analytics."
16
+ />
17
+ <meta property="og:type" content="website" />
18
+ <title>EU Air Traffic</title>
13
19
</head>
14
20
<body>
15
21
<div id="root"></div>
16
22
▼web/src/components/layout/AppShell.tsx
+15-14
7
7
Database,
8
8
GitBranch,
9
9
Globe2,
10
+ Code2,
10
11
Radio,
11
12
Sparkles,
12
13
Table2,
42
43
const location = useLocation();
43
44
const isGlobe = location.pathname === "/";
44
45
const { data: manifest } = useManifest();
46
+ const totalRecords = manifest
47
+ ? Object.values(manifest.counts).reduce((a, b) => a + b, 0)
48
+ : 0;
45
49
46
50
return (
47
51
<div className="flex h-full flex-col overflow-hidden bg-black">
51
55
<Radio className="h-4 w-4" />
52
56
</div>
53
57
<div className="leading-tight">
54
- <div className="flex items-center gap-2">
55
- <span className="text-sm font-semibold tracking-tight text-zinc-100">
56
- EU Air Traffic
57
- </span>
58
- <span className="hidden rounded border border-emerald-500/30 bg-emerald-500/10 px-1.5 py-px text-[9px] font-semibold uppercase tracking-wider text-emerald-400 sm:block">
59
- God&apos;s Eye
60
- </span>
61
- </div>
58
+ <span className="block text-sm font-semibold tracking-tight text-zinc-100">
59
+ EU Air Traffic
60
+ </span>
62
61
<span className="hidden text-[10px] text-zinc-500 md:block">
63
- Medallion · Bronze → Silver → Gold
62
+ European airspace intelligence
64
63
</span>
65
64
</div>
66
65
</div>
87
86
</nav>
88
87
89
88
<div className="flex items-center gap-3">
90
- <span className="hidden items-center gap-1.5 text-xs text-zinc-500 xl:flex">
91
- <GitBranch className="h-3.5 w-3.5" />
92
- {manifest ? `${Object.values(manifest.counts).reduce((a, b) => a + b, 0).toLocaleString()} records` : "connecting…"}
93
- </span>
89
+ {totalRecords > 0 && (
90
+ <span className="hidden items-center gap-1.5 text-xs text-zinc-500 xl:flex">
91
+ <GitBranch className="h-3.5 w-3.5" />
92
+ {totalRecords.toLocaleString()} records
93
+ </span>
94
+ )}
94
95
<Clock />
95
96
<a
96
97
href="https://github.com/swadhinbiswas/air-traffic"
99
100
className="grid h-7 w-7 place-items-center rounded-md border border-white/10 text-zinc-500 transition-colors hover:text-zinc-100"
100
101
aria-label="GitHub"
101
102
>
102
- <Database className="h-3.5 w-3.5" />
103
+ <Code2 className="h-3.5 w-3.5" />
103
104
</a>
104
105
</div>
105
106
</header>
106
107
▼web/src/components/map/WorldMap.tsx
+1-1
143
143
}
144
144
145
145
/**
146
- * The God's Eye View: a MapLibre globe (dark CARTO Earth tiles) with live
146
+ * The live globe view: a MapLibre globe (dark CARTO Earth tiles) with live
147
147
* aircraft, airports, route arcs, aviation weather, precipitation radar and a
148
148
* day/night terminator.
149
149
*/
150
150
▼web/src/pages/AnalyticsPage.tsx
+1-1
163
163
}, [fleet.aircraft, fleet.source]);
164
164
165
165
if (loading) return <Spinner label="Loading analytics" />;
166
- if (error || !analytics) return <ErrorState message={error ?? "No analytics bundle found"} />;
166
+ if (error || !analytics) return <ErrorState message={error ?? "No analytics data available"} />;
167
167
168
168
const topAirports = [...analytics.gold_airport_metrics]
169
169
.sort((a, b) => Number(b.total_flights) - Number(a.total_flights))
170
170
▼web/src/pages/DocsPage.tsx
+1-1
82
82
→ GET /live/snapshot → MapLibre globe / Analytics / Stories
83
83
→ GitHub Actions sink → Bronze/Silver Parquet → Hugging Face
84
84
→ dbt (staging → intermediate → marts → reports) → MotherDuck (Gold)
85
- → static bundle + SQL API → Analytics / Catalog / Explorer / SQL / Ops`}
85
+ → MotherDuck Gold (read-only) + VPS live API → Analytics / Catalog / Explorer / SQL / Ops`}
86
86
</pre>
87
87
</div>
88
88
<div className="mt-3 rounded-lg border border-white/5 bg-white/[0.02] p-4">
89
89
▼web/src/pages/LandingPage.tsx
+52-21
410
410
`${a.callsign || a.hex} · ${a.reg ?? a.type ?? ""} · FL${Math.round((a.altFt ?? 0) / 100)} · ${a.gsKt?.toFixed(0) ?? "—"} kt`
411
411
),
412
412
];
413
- return items.length ? items : ["Awaiting live feed…"];
413
+ return items;
414
414
}, [stories, fleet.aircraft]);
415
415
416
+ const liveCount = fleet.aircraft.length;
416
417
const statusLabel =
417
418
fleet.status === "live"
418
- ? fleet.source === "live"
419
- ? "LIVE ADS-B"
420
- : "LIVE API"
419
+ ? "LIVE"
421
420
: fleet.status === "connecting"
422
421
? "CONNECTING"
423
- : "SNAPSHOT";
422
+ : liveCount > 0
423
+ ? "SNAPSHOT"
424
+ : "OFFLINE";
425
+ const statusTone =
426
+ fleet.status === "live"
427
+ ? "text-emerald-400"
428
+ : fleet.status === "connecting"
429
+ ? "text-amber-400"
430
+ : liveCount > 0
431
+ ? "text-cyan-400"
432
+ : "text-red-400";
433
+ const statusDot =
434
+ fleet.status === "live"
435
+ ? "bg-emerald-400"
436
+ : fleet.status === "connecting"
437
+ ? "bg-amber-400"
438
+ : liveCount > 0
439
+ ? "bg-cyan-400"
440
+ : "bg-red-400";
424
441
425
442
return (
426
443
<div className="relative h-full w-full overflow-hidden bg-black">
439
456
<div className="pointer-events-auto panel flex items-center gap-3 px-3 py-2">
440
457
<span className="relative grid h-7 w-7 place-items-center rounded-lg bg-emerald-500/15 text-emerald-400">
441
458
<Radar className="h-4 w-4" />
442
- <span className="absolute inset-0 rounded-lg text-emerald-400 pulse-ring" />
459
+ {fleet.status === "live" && (
460
+ <span className="absolute inset-0 rounded-lg text-emerald-400 pulse-ring" />
461
+ )}
443
462
</span>
444
463
<div className="leading-tight">
445
464
<div className="text-xs font-semibold text-zinc-100">European Airspace</div>
446
- <div className="mono text-[10px] text-zinc-500">
447
- {statusLabel} · {nf(fleet.aircraft.length)} targets
465
+ <div className="mono flex items-center gap-1.5 text-[10px] text-zinc-500">
466
+ <span className={cn("inline-block h-1.5 w-1.5 rounded-full", statusDot)} />
467
+ <span className={statusTone}>{statusLabel}</span>
468
+ <span className="text-zinc-600">·</span>
469
+ {liveCount > 0 ? `${nf(liveCount)} targets` : "awaiting feed"}
448
470
</div>
449
471
</div>
450
472
</div>
509
531
510
532
{/* Bottom-left: KPI strip */}
511
533
<div className="pointer-events-none absolute bottom-16 left-4 z-10 flex flex-wrap gap-2">
512
- <HudStat label="Live aircraft" value={nf(fleet.aircraft.length)} />
513
- <HudStat label="Cruising >FL250" value={nf(stats.cruising)} />
514
- <HudStat label="Avg altitude" value={`${nf(stats.avgAlt)} ft`} />
534
+ <HudStat label="Live aircraft" value={liveCount ? nf(liveCount) : "—"} />
535
+ <HudStat label="Cruising >FL250" value={liveCount ? nf(stats.cruising) : "—"} />
536
+ <HudStat label="Avg altitude" value={liveCount ? `${nf(stats.avgAlt)} ft` : "—"} />
515
537
<HudStat
516
538
label="Emergencies"
517
- value={nf(stats.emergencies)}
518
- tone={stats.emergencies > 0 ? "text-red-400" : undefined}
539
+ value={liveCount ? nf(stats.emergencies) : "—"}
540
+ tone={liveCount && stats.emergencies > 0 ? "text-red-400" : undefined}
541
+ />
542
+ <HudStat
543
+ label="Weather stns"
544
+ value={weatherState.stations.length ? nf(weatherState.stations.length) : "—"}
519
545
/>
520
- <HudStat label="Weather stns" value={nf(weatherState.stations.length)} />
521
546
<HudStat
522
547
label="Avg delay"
523
548
value={(kpis?.total_flights ?? 0) > 0 ? `${nf(kpis?.avg_delay_minutes ?? 0, 0)} min` : "—"}
569
594
<ArrowUpRight className="h-3 w-3" />
570
595
</Link>
571
596
<div className="relative flex-1 overflow-hidden">
572
- <div className="animate-ticker flex w-max gap-8 whitespace-nowrap">
573
- {[...ticker, ...ticker].map((t, i) => (
574
- <span key={i} className="mono text-[10px] text-zinc-500">
575
- {t}
576
- </span>
577
- ))}
578
- </div>
597
+ {ticker.length ? (
598
+ <div className="animate-ticker flex w-max gap-8 whitespace-nowrap">
599
+ {[...ticker, ...ticker].map((t, i) => (
600
+ <span key={i} className="mono text-[10px] text-zinc-500">
601
+ {t}
602
+ </span>
603
+ ))}
604
+ </div>
605
+ ) : (
606
+ <span className="mono text-[10px] text-zinc-600">
607
+ Awaiting live feed from the VPS collector…
608
+ </span>
609
+ )}
579
610
</div>
580
611
<span className="mono hidden flex-none text-[10px] text-zinc-600 lg:block">
581
612
© CARTO · OSM · RainViewer
582
613