d7ba82e
commit d7ba82e
6 files changed
+79 -41
Showing 6 changed files+79-41
▼web/index.html
+9-3
| @@ -2,14 +2,20 @@ | ||
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,6 +7,7 @@ import { | ||
7 | 7 | Database, |
8 | 8 | GitBranch, |
9 | 9 | Globe2, |
10 | + Code2, | |
10 | 11 | Radio, |
11 | 12 | Sparkles, |
12 | 13 | Table2, |
| @@ -42,6 +43,9 @@ export function AppShell() { | ||
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,16 +55,11 @@ export function AppShell() { | ||
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'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,10 +86,12 @@ export function AppShell() { | ||
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,7 +100,7 @@ export function AppShell() { | ||
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,7 +143,7 @@ function BackgroundClick({ onClear }: { onClear: () => void }) { | ||
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,7 +163,7 @@ export function AnalyticsPage() { | ||
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,7 +82,7 @@ export function DocsPage() { | ||
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,17 +410,34 @@ export function LandingPage() { | ||
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,12 +456,17 @@ export function LandingPage() { | ||
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,15 +531,18 @@ export function LandingPage() { | ||
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,13 +594,19 @@ export function LandingPage() { | ||
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 | |