diff --git a/static/css/adsb_dashboard.css b/static/css/adsb_dashboard.css index 5ee91ee..b581a3f 100644 --- a/static/css/adsb_dashboard.css +++ b/static/css/adsb_dashboard.css @@ -136,12 +136,6 @@ body { font-size: 11px; } -.status-item { - display: flex; - align-items: center; - gap: 6px; -} - .status-dot { width: 8px; height: 8px; @@ -168,37 +162,6 @@ body { } } -/* Stats badges in header */ -.stats-badges { - display: flex; - gap: 12px; -} - -.stat-badge { - background: rgba(74, 158, 255, 0.1); - border: 1px solid rgba(74, 158, 255, 0.3); - border-radius: 4px; - padding: 4px 10px; - font-family: 'JetBrains Mono', monospace; - font-size: 11px; -} - -.stat-badge .value { - color: var(--accent-cyan); - font-weight: 600; -} - -.stat-badge .label { - color: var(--text-secondary); - margin-left: 4px; -} - -.datetime { - font-family: 'Orbitron', monospace; - font-size: 12px; - color: var(--accent-cyan); -} - .back-link { color: var(--accent-cyan); text-decoration: none; @@ -737,43 +700,110 @@ body { } .control-group { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 4px; + padding: 6px 10px; + background: rgba(74, 158, 255, 0.03); + border: 1px solid rgba(74, 158, 255, 0.1); + border-radius: 6px; +} + +.control-group-label { + font-size: 8px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 1px; + color: var(--accent-cyan); + opacity: 0.7; +} + +.control-group-items { display: flex; align-items: center; - gap: 8px; + gap: 6px; + flex-wrap: wrap; } .control-group label { display: flex; align-items: center; - gap: 6px; + gap: 4px; cursor: pointer; - font-size: 11px; + font-size: 10px; color: var(--text-primary); + white-space: nowrap; } .control-group input[type="checkbox"] { accent-color: var(--accent-cyan); + width: 12px; + height: 12px; } .control-group select { - padding: 6px 10px; + padding: 4px 8px; background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(74, 158, 255, 0.3); border-radius: 4px; color: var(--accent-cyan); font-family: 'JetBrains Mono', monospace; - font-size: 11px; + font-size: 10px; } -.control-group input[type="text"] { - width: 80px; - padding: 6px 8px; +.control-group input[type="text"], +.control-group input[type="number"] { + padding: 4px 6px; background: rgba(0, 0, 0, 0.3); border: 1px solid rgba(74, 158, 255, 0.3); border-radius: 4px; color: var(--accent-cyan); font-family: 'JetBrains Mono', monospace; - font-size: 11px; + font-size: 10px; +} + +.control-group.tracking-group { + background: rgba(34, 197, 94, 0.05); + border-color: rgba(34, 197, 94, 0.2); +} + +.control-group.tracking-group .control-group-label { + color: var(--accent-green); +} + +.control-group.airband-group { + background: rgba(245, 158, 11, 0.05); + border-color: rgba(245, 158, 11, 0.2); +} + +.control-group.airband-group .control-group-label { + color: var(--accent-orange); +} + +.airband-sliders { + display: flex; + align-items: center; + gap: 4px; + font-size: 8px; + color: var(--text-dim); +} + +.airband-sliders input[type="range"] { + width: 40px; + height: 4px; + -webkit-appearance: none; + background: rgba(74, 158, 255, 0.2); + border-radius: 2px; +} + +.airband-sliders input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 10px; + height: 10px; + background: var(--accent-cyan); + border-radius: 50%; + cursor: pointer; } .control-label { @@ -785,19 +815,18 @@ body { /* Start/stop button */ .start-btn { - padding: 8px 20px; + padding: 6px 16px; border: none; background: var(--accent-green); color: #fff; - font-family: 'Orbitron', monospace; - font-size: 11px; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; font-weight: 600; text-transform: uppercase; - letter-spacing: 2px; + letter-spacing: 1px; border-radius: 4px; cursor: pointer; transition: all 0.2s ease; - margin-left: auto; } .start-btn:hover { @@ -1155,8 +1184,8 @@ body { gap: 6px; } - /* DateTime smaller */ - .datetime { + /* Strip time smaller on mobile */ + .strip-time { font-size: 10px; } @@ -1233,3 +1262,599 @@ body { line-height: 44px !important; font-size: 18px !important; } + +/* ============================================ + STATISTICS STRIP + ============================================ */ +.stats-strip { + background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%); + border-bottom: 1px solid var(--border-color); + padding: 6px 12px; + position: relative; + z-index: 9; + overflow-x: auto; +} + +.stats-strip-inner { + display: flex; + align-items: center; + gap: 4px; + min-width: max-content; +} + +.strip-stat { + display: flex; + flex-direction: column; + align-items: center; + padding: 4px 10px; + background: rgba(74, 158, 255, 0.05); + border: 1px solid rgba(74, 158, 255, 0.15); + border-radius: 4px; + min-width: 55px; +} + +.strip-stat:hover { + background: rgba(74, 158, 255, 0.1); + border-color: rgba(74, 158, 255, 0.3); +} + +.strip-value { + font-family: 'JetBrains Mono', monospace; + font-size: 14px; + font-weight: 600; + color: var(--accent-cyan); + line-height: 1.2; +} + +.strip-label { + font-size: 8px; + font-weight: 600; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-top: 1px; +} + +.strip-stat.session-stat { + background: rgba(34, 197, 94, 0.05); + border-color: rgba(34, 197, 94, 0.2); +} + +.strip-stat.session-stat .strip-value { + color: var(--accent-green); +} + +.strip-report-btn { + background: linear-gradient(135deg, var(--accent-cyan) 0%, #2563eb 100%); + border: none; + color: white; + padding: 8px 12px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + cursor: pointer; + margin-left: auto; + white-space: nowrap; + transition: all 0.2s; +} + +.strip-report-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(74, 158, 255, 0.3); +} + +/* ============================================ + REPORT MODAL + ============================================ */ +.report-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + padding: 20px; +} + +.report-content { + background: var(--bg-panel); + border: 1px solid var(--border-color); + border-radius: 12px; + width: 100%; + max-width: 600px; + max-height: 85vh; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); +} + +.report-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + background: var(--bg-card); + border-bottom: 1px solid var(--border-color); +} + +.report-header h2 { + font-size: 16px; + font-weight: 600; + color: var(--accent-cyan); +} + +.report-close { + background: none; + border: none; + color: var(--text-secondary); + font-size: 24px; + cursor: pointer; + padding: 0 8px; + line-height: 1; +} + +.report-close:hover { + color: var(--text-primary); +} + +.report-body { + flex: 1; + overflow-y: auto; + padding: 16px 20px; +} + +.report-section { + margin-bottom: 20px; +} + +.report-section h3 { + font-size: 12px; + font-weight: 600; + color: var(--accent-cyan); + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 10px; + padding-bottom: 6px; + border-bottom: 1px solid var(--border-color); +} + +.report-grid { + display: grid; + grid-template-columns: auto 1fr; + gap: 6px 16px; + font-size: 12px; +} + +.report-grid span:nth-child(odd) { + color: var(--text-secondary); +} + +.report-grid span:nth-child(even) { + color: var(--text-primary); + font-family: 'JetBrains Mono', monospace; +} + +.report-highlights { + display: flex; + flex-direction: column; + gap: 6px; +} + +.highlight-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + background: var(--bg-card); + border-radius: 4px; + font-size: 11px; +} + +.highlight-item.military { + border-left: 3px solid #556b2f; +} + +.highlight-item.emergency { + border-left: 3px solid var(--accent-red); +} + +.highlight-type { + font-size: 9px; + font-weight: 700; + padding: 2px 6px; + border-radius: 3px; + background: rgba(255, 255, 255, 0.1); +} + +.highlight-item.military .highlight-type { + background: rgba(85, 107, 47, 0.3); + color: #8fbc8f; +} + +.highlight-item.emergency .highlight-type { + background: rgba(239, 68, 68, 0.3); + color: #f87171; +} + +.highlight-detail { + color: var(--text-primary); +} + +.highlight-more { + font-size: 10px; + color: var(--text-dim); + text-align: center; + padding: 4px; +} + +.report-table-wrap { + overflow-x: auto; + max-height: 200px; + overflow-y: auto; +} + +.report-table { + width: 100%; + border-collapse: collapse; + font-size: 11px; +} + +.report-table th { + background: var(--bg-card); + color: var(--text-secondary); + font-weight: 600; + text-align: left; + padding: 8px 10px; + position: sticky; + top: 0; +} + +.report-table td { + padding: 6px 10px; + border-bottom: 1px solid var(--border-color); + color: var(--text-primary); +} + +.report-table tr.military { + background: rgba(85, 107, 47, 0.1); +} + +.report-table tr:hover { + background: rgba(74, 158, 255, 0.05); +} + +.report-more { + font-size: 10px; + color: var(--text-dim); + text-align: center; + padding: 8px; +} + +.report-footer { + display: flex; + gap: 10px; + padding: 16px 20px; + background: var(--bg-card); + border-top: 1px solid var(--border-color); +} + +.report-btn { + flex: 1; + padding: 10px 16px; + border: 1px solid var(--border-color); + background: var(--bg-panel); + color: var(--text-primary); + border-radius: 6px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.report-btn:hover { + background: var(--bg-card); + border-color: var(--accent-cyan); +} + +/* Mobile responsiveness for stats strip */ +@media (max-width: 768px) { + .stats-strip { + padding: 4px 8px; + } + + .strip-stat { + padding: 3px 6px; + min-width: 45px; + } + + .strip-value { + font-size: 12px; + } + + .strip-label { + font-size: 7px; + } + + .strip-report-btn, + .strip-btn { + padding: 6px 10px; + font-size: 9px; + } + + .report-content { + max-height: 90vh; + } +} + +/* ============================================ + STRIP BUTTONS + ============================================ */ +.strip-divider { + width: 1px; + height: 24px; + background: rgba(74, 158, 255, 0.2); + margin: 0 4px; +} + +.strip-btn { + background: rgba(74, 158, 255, 0.1); + border: 1px solid rgba(74, 158, 255, 0.2); + color: var(--text-primary); + padding: 6px 10px; + border-radius: 4px; + font-size: 10px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; +} + +.strip-btn:hover:not(:disabled) { + background: rgba(74, 158, 255, 0.2); + border-color: rgba(74, 158, 255, 0.4); +} + +.strip-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.strip-btn.primary { + background: linear-gradient(135deg, var(--accent-cyan) 0%, #2563eb 100%); + border: none; + color: white; +} + +.strip-btn.primary:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(74, 158, 255, 0.3); +} + +/* Status and time in strip */ +.strip-status { + display: flex; + align-items: center; + gap: 6px; + padding: 0 8px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-secondary); +} + +.strip-status .status-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--accent-red); + box-shadow: 0 0 10px var(--accent-red); + animation: pulse 2s ease-in-out infinite; + transition: all 0.3s ease; +} + +.strip-status .status-dot.active { + background: var(--accent-green); + box-shadow: 0 0 10px var(--accent-green); +} + +.strip-time { + font-size: 11px; + font-weight: 500; + color: var(--accent-cyan); + font-family: 'JetBrains Mono', monospace; + padding-left: 8px; + border-left: 1px solid rgba(74, 158, 255, 0.2); + white-space: nowrap; +} + +/* Signal quality states */ +.strip-stat.signal-stat .strip-value { + letter-spacing: 2px; +} + +.strip-stat.signal-stat.good { + background: rgba(34, 197, 94, 0.1); + border-color: rgba(34, 197, 94, 0.3); +} + +.strip-stat.signal-stat.good .strip-value { + color: var(--accent-green); +} + +.strip-stat.signal-stat.warning { + background: rgba(245, 158, 11, 0.1); + border-color: rgba(245, 158, 11, 0.3); +} + +.strip-stat.signal-stat.warning .strip-value { + color: var(--accent-orange); +} + +.strip-stat.signal-stat.poor { + background: rgba(239, 68, 68, 0.1); + border-color: rgba(239, 68, 68, 0.3); +} + +.strip-stat.signal-stat.poor .strip-value { + color: var(--accent-red); +} + +/* ============================================ + SQUAWK REFERENCE MODAL + ============================================ */ +.squawk-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + padding: 20px; +} + +.squawk-content { + background: var(--bg-panel); + border: 1px solid var(--border-color); + border-radius: 12px; + width: 100%; + max-width: 650px; + max-height: 85vh; + display: flex; + flex-direction: column; + overflow: hidden; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); +} + +.squawk-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + background: var(--bg-card); + border-bottom: 1px solid var(--border-color); +} + +.squawk-header h2 { + font-size: 16px; + font-weight: 600; + color: var(--accent-cyan); +} + +.squawk-close { + background: none; + border: none; + color: var(--text-secondary); + font-size: 24px; + cursor: pointer; + padding: 0 8px; + line-height: 1; +} + +.squawk-close:hover { + color: var(--text-primary); +} + +.squawk-body { + flex: 1; + overflow-y: auto; + padding: 16px 20px; +} + +.squawk-section { + margin-bottom: 16px; +} + +.squawk-section h3 { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 1px; + margin-bottom: 10px; + padding-bottom: 6px; + border-bottom: 1px solid var(--border-color); +} + +.squawk-section.emergency h3 { + color: var(--accent-red); + border-color: rgba(239, 68, 68, 0.3); +} + +.squawk-section.special h3 { + color: var(--accent-orange); + border-color: rgba(245, 158, 11, 0.3); +} + +.squawk-section.standard h3 { + color: var(--accent-cyan); +} + +.squawk-section.other h3 { + color: var(--text-secondary); +} + +.squawk-grid { + display: flex; + flex-direction: column; + gap: 6px; +} + +.squawk-item { + display: grid; + grid-template-columns: 50px 100px 1fr; + gap: 12px; + align-items: center; + padding: 8px 10px; + background: var(--bg-card); + border-radius: 4px; + font-size: 11px; +} + +.squawk-code { + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + color: var(--accent-cyan); + font-size: 12px; +} + +.squawk-section.emergency .squawk-code { + color: var(--accent-red); +} + +.squawk-section.special .squawk-code { + color: var(--accent-orange); +} + +.squawk-name { + font-weight: 600; + color: var(--text-primary); + font-size: 10px; +} + +.squawk-desc { + color: var(--text-secondary); + font-size: 10px; +} + +@media (max-width: 600px) { + .squawk-item { + grid-template-columns: 45px 80px 1fr; + gap: 8px; + font-size: 10px; + } + + .squawk-code { + font-size: 11px; + } +} diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html index fe267df..e520924 100644 --- a/templates/adsb_dashboard.html +++ b/templates/adsb_dashboard.html @@ -19,30 +19,73 @@ AIRCRAFT RADAR // INTERCEPT - See the Invisible -
-
- 0 - aircraft -
-
- 0 - nm max -
-
- 0 - msg/s -
-
-
-
- STANDBY -
-
--:--:-- UTC
Main Dashboard
+ +
+
+
+ 0 + NOW +
+
+ 0 + SEEN +
+
+ 0 + MAX NM +
+
+ - + HIGH FL +
+
+ - + FAST KT +
+
+ - + NEAR NM +
+
+ 0 + COUNTRIES +
+
+ 0 + ACARS +
+
+ -- + SIGNAL +
+
+ 00:00:00 + SESSION +
+
+ + + +
+
+
+ STANDBY +
+
--:--:-- UTC
+
+
+
@@ -137,77 +180,97 @@
- +
- - - - - - - - - - - - - ADS-B: - - - -
- - Listen: - - - - - - - 🔊 - - - - OFF - -
@@ -245,9 +308,163 @@ totalAircraftSeen: new Set(), maxRange: 0, messagesPerSecond: 0, - messageTimestamps: [] + messageTimestamps: [], + countriesSeen: new Set(), + highestAltitude: 0, + fastestSpeed: 0, + closestDistance: Infinity, + sessionStart: null, + acarsMessages: 0 }; + // Session log for report generation + let sessionLog = { + startTime: null, + endTime: null, + aircraftLog: [], // Array of all aircraft seen with details + highlights: [], // Notable events (military, emergency, etc) + maxConcurrent: 0, + peakMsgRate: 0 + }; + + // ICAO Country Allocations (first hex digit ranges) + const ICAO_COUNTRY_RANGES = [ + { start: 0x000000, end: 0x003FFF, country: 'Zimbabwe' }, + { start: 0x004000, end: 0x0043FF, country: 'Mozambique' }, + { start: 0x006000, end: 0x006FFF, country: 'South Africa' }, + { start: 0x008000, end: 0x00FFFF, country: 'South Africa' }, + { start: 0x010000, end: 0x017FFF, country: 'Egypt' }, + { start: 0x018000, end: 0x01FFFF, country: 'Libya' }, + { start: 0x020000, end: 0x027FFF, country: 'Morocco' }, + { start: 0x028000, end: 0x02FFFF, country: 'Tunisia' }, + { start: 0x030000, end: 0x0303FF, country: 'Botswana' }, + { start: 0x032000, end: 0x032FFF, country: 'Burundi' }, + { start: 0x034000, end: 0x034FFF, country: 'Cameroon' }, + { start: 0x038000, end: 0x038FFF, country: 'Congo' }, + { start: 0x03E000, end: 0x03EFFF, country: 'Gabon' }, + { start: 0x040000, end: 0x040FFF, country: 'Ethiopia' }, + { start: 0x042000, end: 0x042FFF, country: 'Equatorial Guinea' }, + { start: 0x044000, end: 0x044FFF, country: 'Ghana' }, + { start: 0x048000, end: 0x0483FF, country: 'Tanzania' }, + { start: 0x050000, end: 0x050FFF, country: 'Kenya' }, + { start: 0x054000, end: 0x054FFF, country: 'Zambia' }, + { start: 0x058000, end: 0x058FFF, country: 'Seychelles' }, + { start: 0x060000, end: 0x061FFF, country: 'Algeria' }, + { start: 0x068000, end: 0x068FFF, country: 'Angola' }, + { start: 0x070000, end: 0x070FFF, country: 'Ivory Coast' }, + { start: 0x078000, end: 0x078FFF, country: 'Mauritius' }, + { start: 0x080000, end: 0x080FFF, country: 'Nigeria' }, + { start: 0x088000, end: 0x088FFF, country: 'Uganda' }, + { start: 0x090000, end: 0x090FFF, country: 'Qatar' }, + { start: 0x0A0000, end: 0x0A7FFF, country: 'India' }, + { start: 0x0C0000, end: 0x0C4FFF, country: 'Australia' }, + { start: 0x100000, end: 0x1FFFFF, country: 'Russia' }, + { start: 0x200000, end: 0x27FFFF, country: 'USA' }, + { start: 0x280000, end: 0x28FFFF, country: 'USA' }, + { start: 0x300000, end: 0x33FFFF, country: 'Italy' }, + { start: 0x340000, end: 0x37FFFF, country: 'Spain' }, + { start: 0x380000, end: 0x3BFFFF, country: 'France' }, + { start: 0x3C0000, end: 0x3FFFFF, country: 'Germany' }, + { start: 0x400000, end: 0x43FFFF, country: 'UK' }, + { start: 0x440000, end: 0x447FFF, country: 'Austria' }, + { start: 0x448000, end: 0x44FFFF, country: 'Belgium' }, + { start: 0x450000, end: 0x457FFF, country: 'Bulgaria' }, + { start: 0x458000, end: 0x45FFFF, country: 'Denmark' }, + { start: 0x460000, end: 0x467FFF, country: 'Finland' }, + { start: 0x468000, end: 0x46FFFF, country: 'Greece' }, + { start: 0x470000, end: 0x477FFF, country: 'Hungary' }, + { start: 0x478000, end: 0x47FFFF, country: 'Norway' }, + { start: 0x480000, end: 0x487FFF, country: 'Netherlands' }, + { start: 0x488000, end: 0x48FFFF, country: 'Poland' }, + { start: 0x490000, end: 0x497FFF, country: 'Portugal' }, + { start: 0x498000, end: 0x49FFFF, country: 'Czech Republic' }, + { start: 0x4A0000, end: 0x4A7FFF, country: 'Romania' }, + { start: 0x4A8000, end: 0x4AFFFF, country: 'Sweden' }, + { start: 0x4B0000, end: 0x4B7FFF, country: 'Switzerland' }, + { start: 0x4B8000, end: 0x4BFFFF, country: 'Turkey' }, + { start: 0x4C0000, end: 0x4C7FFF, country: 'Serbia' }, + { start: 0x4CA000, end: 0x4CAFFF, country: 'Ireland' }, + { start: 0x4D0000, end: 0x4D03FF, country: 'Iceland' }, + { start: 0x500000, end: 0x5003FF, country: 'Luxembourg' }, + { start: 0x501000, end: 0x5013FF, country: 'Monaco' }, + { start: 0x502000, end: 0x502FFF, country: 'Malta' }, + { start: 0x503000, end: 0x5033FF, country: 'San Marino' }, + { start: 0x505000, end: 0x5057FF, country: 'Latvia' }, + { start: 0x506000, end: 0x5067FF, country: 'Lithuania' }, + { start: 0x507000, end: 0x5077FF, country: 'Moldova' }, + { start: 0x508000, end: 0x50FFFF, country: 'Slovakia' }, + { start: 0x510000, end: 0x5107FF, country: 'Slovenia' }, + { start: 0x511000, end: 0x5117FF, country: 'Uzbekistan' }, + { start: 0x512000, end: 0x5127FF, country: 'Ukraine' }, + { start: 0x513000, end: 0x5137FF, country: 'Belarus' }, + { start: 0x514000, end: 0x5147FF, country: 'Estonia' }, + { start: 0x515000, end: 0x5157FF, country: 'Macedonia' }, + { start: 0x516000, end: 0x5167FF, country: 'Bosnia' }, + { start: 0x517000, end: 0x5177FF, country: 'Georgia' }, + { start: 0x518000, end: 0x5187FF, country: 'Tajikistan' }, + { start: 0x600000, end: 0x6003FF, country: 'Armenia' }, + { start: 0x680000, end: 0x6803FF, country: 'Kyrgyzstan' }, + { start: 0x681000, end: 0x6813FF, country: 'Turkmenistan' }, + { start: 0x682000, end: 0x6823FF, country: 'Azerbaijan' }, + { start: 0x683000, end: 0x6833FF, country: 'Kazakhstan' }, + { start: 0x700000, end: 0x700FFF, country: 'Afghanistan' }, + { start: 0x702000, end: 0x702FFF, country: 'Bangladesh' }, + { start: 0x704000, end: 0x704FFF, country: 'Maldives' }, + { start: 0x706000, end: 0x706FFF, country: 'Nepal' }, + { start: 0x708000, end: 0x708FFF, country: 'Pakistan' }, + { start: 0x70A000, end: 0x70AFFF, country: 'Sri Lanka' }, + { start: 0x70C000, end: 0x70C3FF, country: 'Myanmar' }, + { start: 0x710000, end: 0x717FFF, country: 'Japan' }, + { start: 0x718000, end: 0x71FFFF, country: 'Japan' }, + { start: 0x720000, end: 0x727FFF, country: 'Laos' }, + { start: 0x728000, end: 0x72FFFF, country: 'Mongolia' }, + { start: 0x730000, end: 0x737FFF, country: 'Nepal' }, + { start: 0x738000, end: 0x73FFFF, country: 'South Korea' }, + { start: 0x740000, end: 0x747FFF, country: 'Indonesia' }, + { start: 0x748000, end: 0x74FFFF, country: 'Malaysia' }, + { start: 0x750000, end: 0x757FFF, country: 'Philippines' }, + { start: 0x758000, end: 0x75FFFF, country: 'Singapore' }, + { start: 0x760000, end: 0x767FFF, country: 'Thailand' }, + { start: 0x768000, end: 0x76FFFF, country: 'Vietnam' }, + { start: 0x780000, end: 0x7BFFFF, country: 'China' }, + { start: 0x7C0000, end: 0x7FFFFF, country: 'Australia' }, + { start: 0x800000, end: 0x83FFFF, country: 'India' }, + { start: 0x840000, end: 0x87FFFF, country: 'Japan' }, + { start: 0x880000, end: 0x887FFF, country: 'Pakistan' }, + { start: 0x890000, end: 0x890FFF, country: 'Hong Kong' }, + { start: 0x894000, end: 0x894FFF, country: 'Taiwan' }, + { start: 0x895000, end: 0x8953FF, country: 'North Korea' }, + { start: 0x896000, end: 0x896FFF, country: 'Jordan' }, + { start: 0x897000, end: 0x897FFF, country: 'Lebanon' }, + { start: 0x898000, end: 0x898FFF, country: 'Kuwait' }, + { start: 0x899000, end: 0x8993FF, country: 'Saudi Arabia' }, + { start: 0x8A0000, end: 0x8A7FFF, country: 'Saudi Arabia' }, + { start: 0x900000, end: 0x9003FF, country: 'Kuwait' }, + { start: 0x901000, end: 0x9013FF, country: 'Bahrain' }, + { start: 0x902000, end: 0x9023FF, country: 'Yemen' }, + { start: 0x903000, end: 0x9033FF, country: 'Syria' }, + { start: 0xA00000, end: 0xAFFFFF, country: 'USA' }, + { start: 0xC00000, end: 0xC3FFFF, country: 'Canada' }, + { start: 0xC80000, end: 0xC87FFF, country: 'New Zealand' }, + { start: 0xE00000, end: 0xE3FFFF, country: 'Argentina' }, + { start: 0xE40000, end: 0xE7FFFF, country: 'Brazil' }, + { start: 0xE80000, end: 0xE80FFF, country: 'Chile' }, + { start: 0xE84000, end: 0xE84FFF, country: 'Colombia' }, + { start: 0xE88000, end: 0xE88FFF, country: 'Peru' }, + { start: 0xE8C000, end: 0xE8CFFF, country: 'Venezuela' }, + { start: 0xF00000, end: 0xF07FFF, country: 'ICAO (special)' } + ]; + + function getCountryFromIcao(icao) { + const icaoNum = parseInt(icao, 16); + for (const range of ICAO_COUNTRY_RANGES) { + if (icaoNum >= range.start && icaoNum <= range.end) { + return range.country; + } + } + return 'Unknown'; + } + // Observer location and range rings (load from localStorage or default to London) let observerLocation = (function() { const saved = localStorage.getItem('observerLocation'); @@ -568,30 +785,411 @@ // STATISTICS // ============================================ function updateStatistics(icao, ac) { - if (!ac.lat || !ac.lon) return; + const isNew = !stats.totalAircraftSeen.has(icao); stats.totalAircraftSeen.add(icao); - const distance = calculateDistanceNm( - observerLocation.lat, observerLocation.lon, - ac.lat, ac.lon - ); - - if (distance > stats.maxRange) { - stats.maxRange = distance; + // Track country + const country = getCountryFromIcao(icao); + if (country !== 'Unknown') { + stats.countriesSeen.add(country); } + // Log new aircraft + if (isNew) { + const militaryInfo = isMilitaryAircraft(icao, ac.callsign); + const squawkInfo = checkSquawkCode(ac); + sessionLog.aircraftLog.push({ + icao, + callsign: ac.callsign || '', + registration: ac.registration || '', + country, + military: militaryInfo.military, + firstSeen: new Date().toISOString(), + altitude: ac.altitude, + speed: ac.speed + }); + + // Log highlights + if (militaryInfo.military) { + sessionLog.highlights.push({ + time: new Date().toISOString(), + type: 'military', + icao, + callsign: ac.callsign || icao, + country: militaryInfo.country || country + }); + } + if (squawkInfo && squawkInfo.type === 'emergency') { + sessionLog.highlights.push({ + time: new Date().toISOString(), + type: 'emergency', + icao, + callsign: ac.callsign || icao, + squawk: ac.squawk, + name: squawkInfo.name + }); + } + } + + // Distance calculation + if (ac.lat && ac.lon) { + const distance = calculateDistanceNm( + observerLocation.lat, observerLocation.lon, + ac.lat, ac.lon + ); + + if (distance > stats.maxRange) { + stats.maxRange = distance; + } + } + + // Message rate const now = Date.now(); stats.messageTimestamps.push(now); stats.messageTimestamps = stats.messageTimestamps.filter(t => now - t < 5000); stats.messagesPerSecond = stats.messageTimestamps.length / 5; + // Track peak message rate + if (stats.messagesPerSecond > sessionLog.peakMsgRate) { + sessionLog.peakMsgRate = stats.messagesPerSecond; + } + updateStatsDisplay(); } + // Signal quality tracking + let signalStats = { + goodMessages: 0, + errorMessages: 0 + }; + function updateStatsDisplay() { - document.getElementById('statMaxRange').textContent = stats.maxRange.toFixed(0); - document.getElementById('statMsgRate').textContent = stats.messagesPerSecond.toFixed(1); - document.getElementById('statTotal').textContent = Object.keys(aircraft).length; + const aircraftCount = Object.keys(aircraft).length; + + // Track max concurrent + if (aircraftCount > sessionLog.maxConcurrent) { + sessionLog.maxConcurrent = aircraftCount; + } + + // Calculate live stats from current aircraft + let highest = 0, fastest = 0, closest = Infinity; + let highestIcao = '', fastestIcao = '', closestIcao = ''; + + Object.entries(aircraft).forEach(([icao, ac]) => { + if (ac.altitude && ac.altitude > highest) { + highest = ac.altitude; + highestIcao = icao; + } + if (ac.speed && ac.speed > fastest) { + fastest = ac.speed; + fastestIcao = icao; + } + if (ac.lat && ac.lon) { + const dist = calculateDistanceNm( + observerLocation.lat, observerLocation.lon, + ac.lat, ac.lon + ); + if (dist < closest) { + closest = dist; + closestIcao = icao; + } + } + }); + + // Update strip stats + document.getElementById('stripAircraftNow').textContent = aircraftCount; + document.getElementById('stripTotalSeen').textContent = stats.totalAircraftSeen.size; + document.getElementById('stripMaxRange').textContent = stats.maxRange.toFixed(0); + document.getElementById('stripHighest').textContent = highest > 0 ? Math.round(highest / 100) : '-'; + document.getElementById('stripFastest').textContent = fastest > 0 ? Math.round(fastest) : '-'; + document.getElementById('stripClosest').textContent = closest < Infinity ? closest.toFixed(1) : '-'; + document.getElementById('stripCountries').textContent = stats.countriesSeen.size; + document.getElementById('stripAcars').textContent = stats.acarsMessages; + + // Update signal quality + updateSignalQuality(); + } + + // Session timer + let sessionTimerInterval = null; + function startSessionTimer() { + if (!stats.sessionStart) { + stats.sessionStart = Date.now(); + sessionLog.startTime = new Date().toISOString(); + } + if (sessionTimerInterval) clearInterval(sessionTimerInterval); + sessionTimerInterval = setInterval(updateSessionTimer, 1000); + } + + function stopSessionTimer() { + sessionLog.endTime = new Date().toISOString(); + } + + function updateSessionTimer() { + if (!stats.sessionStart) return; + const elapsed = Date.now() - stats.sessionStart; + const hours = Math.floor(elapsed / 3600000); + const mins = Math.floor((elapsed % 3600000) / 60000); + const secs = Math.floor((elapsed % 60000) / 1000); + document.getElementById('stripSession').textContent = + `${hours.toString().padStart(2,'0')}:${mins.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')}`; + } + + // Report generation + function generateReport() { + stopSessionTimer(); + + const report = { + title: 'ADS-B Session Report', + generated: new Date().toISOString(), + session: { + start: sessionLog.startTime, + end: sessionLog.endTime || new Date().toISOString(), + duration: stats.sessionStart ? formatDuration(Date.now() - stats.sessionStart) : 'N/A' + }, + location: { + lat: observerLocation.lat, + lon: observerLocation.lon + }, + statistics: { + totalAircraftSeen: stats.totalAircraftSeen.size, + maxConcurrent: sessionLog.maxConcurrent, + maxRange: stats.maxRange.toFixed(1) + ' nm', + peakMessageRate: sessionLog.peakMsgRate.toFixed(1) + ' msg/s', + countriesSeen: Array.from(stats.countriesSeen).sort(), + acarsMessages: stats.acarsMessages + }, + highlights: sessionLog.highlights, + aircraftLog: sessionLog.aircraftLog + }; + + // Show report modal + showReportModal(report); + } + + function formatDuration(ms) { + const hours = Math.floor(ms / 3600000); + const mins = Math.floor((ms % 3600000) / 60000); + const secs = Math.floor((ms % 60000) / 1000); + return `${hours}h ${mins}m ${secs}s`; + } + + function showReportModal(report) { + const modal = document.createElement('div'); + modal.className = 'report-modal'; + modal.innerHTML = ` +
+
+

📊 Session Report

+ +
+
+
+

Session Info

+
+ Duration:${report.session.duration} + Location:${report.location.lat.toFixed(4)}, ${report.location.lon.toFixed(4)} +
+
+
+

Statistics

+
+ Total Aircraft:${report.statistics.totalAircraftSeen} + Max Concurrent:${report.statistics.maxConcurrent} + Max Range:${report.statistics.maxRange} + Peak Msg Rate:${report.statistics.peakMessageRate} + Countries:${report.statistics.countriesSeen.length} (${report.statistics.countriesSeen.slice(0,5).join(', ')}${report.statistics.countriesSeen.length > 5 ? '...' : ''}) + ACARS Messages:${report.statistics.acarsMessages} +
+
+ ${report.highlights.length > 0 ? ` +
+

Highlights

+
+ ${report.highlights.slice(0, 10).map(h => ` +
+ ${h.type.toUpperCase()} + ${h.callsign}${h.country ? ' (' + h.country + ')' : ''}${h.name ? ' - ' + h.name : ''} +
+ `).join('')} + ${report.highlights.length > 10 ? `
+${report.highlights.length - 10} more...
` : ''} +
+
+ ` : ''} +
+

Aircraft Log (${report.aircraftLog.length})

+
+ + + + + + ${report.aircraftLog.slice(0, 50).map(ac => ` + + + + + + + `).join('')} + +
ICAOCallsignCountryType
${ac.icao}${ac.callsign || '-'}${ac.country}${ac.military ? '🎖️ MIL' : 'CIV'}
+ ${report.aircraftLog.length > 50 ? `
Showing 50 of ${report.aircraftLog.length} aircraft
` : ''} +
+
+
+ +
+ `; + document.body.appendChild(modal); + + // Store report for download + window._currentReport = report; + } + + function downloadReport() { + if (!window._currentReport) return; + const blob = new Blob([JSON.stringify(window._currentReport, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `adsb-report-${new Date().toISOString().slice(0,10)}.json`; + a.click(); + URL.revokeObjectURL(url); + } + + function copyReportToClipboard() { + if (!window._currentReport) return; + const r = window._currentReport; + const summary = `ADS-B Session Report +Duration: ${r.session.duration} +Aircraft Seen: ${r.statistics.totalAircraftSeen} +Max Concurrent: ${r.statistics.maxConcurrent} +Max Range: ${r.statistics.maxRange} +Countries: ${r.statistics.countriesSeen.join(', ')} +Highlights: ${r.highlights.length} events +ACARS: ${r.statistics.acarsMessages} messages`; + navigator.clipboard.writeText(summary).then(() => { + alert('Summary copied to clipboard'); + }); + } + + // ============================================ + // SIGNAL QUALITY + // ============================================ + function updateSignalQuality() { + const msgRate = stats.messagesPerSecond; + const el = document.getElementById('stripSignal'); + const stat = el.closest('.strip-stat'); + + if (!isTracking || msgRate === 0) { + el.textContent = '--'; + stat.classList.remove('good', 'warning', 'poor'); + return; + } + + // Signal quality based on message rate + // Good: >10 msg/s, Warning: 2-10, Poor: <2 + if (msgRate >= 10) { + el.textContent = '●●●'; + stat.classList.remove('warning', 'poor'); + stat.classList.add('good'); + } else if (msgRate >= 2) { + el.textContent = '●●○'; + stat.classList.remove('good', 'poor'); + stat.classList.add('warning'); + } else { + el.textContent = '●○○'; + stat.classList.remove('good', 'warning'); + stat.classList.add('poor'); + } + } + + // ============================================ + // SQUAWK CODE REFERENCE + // ============================================ + function showSquawkReference() { + const modal = document.createElement('div'); + modal.className = 'squawk-modal'; + modal.innerHTML = ` +
+
+

📟 Squawk Code Reference

+ +
+
+
+

🚨 Emergency Codes

+
+
7500HIJACKAircraft being hijacked - do not acknowledge
+
7600RADIO FAILTwo-way radio communication failure
+
7700EMERGENCYGeneral emergency (mayday/pan-pan)
+
+
+
+

⚠️ Special Codes

+
+
7777MIL INTERCEPTActive military intercept operations
+
0000DISCRETEMilitary/special operations
+
5000MILITARY UKUK military low-level operations
+
0033PARA OPSParachute dropping operations
+
+
+
+

✈️ Standard VFR/IFR

+
+
1200VFR (US/CA)Visual flight rules - North America
+
7000VFR (EU)Visual flight rules - ICAO/Europe
+
2000CONSPICUITYEntering airspace, no code assigned
+
1000IFR (EU)Instrument flight rules, no assigned code
+
+
+
+

📋 Other Codes

+
+
4000FERRYAircraft delivery/repositioning
+
7001VFR INTRUSIONVFR aircraft entering controlled space
+
7004AEROBATICAerobatic display flight
+
7010RADIO EQUIPPEDIFR flight (UK zones)
+
+
+
+
+ `; + document.body.appendChild(modal); + } + + // ============================================ + // FLIGHT LOOKUP + // ============================================ + function lookupSelectedFlight() { + if (!selectedIcao || !aircraft[selectedIcao]) return; + const ac = aircraft[selectedIcao]; + const callsign = ac.callsign?.trim(); + const reg = ac.registration?.trim(); + + // Prefer callsign, then registration, then ICAO + let searchTerm = callsign || reg || selectedIcao; + + // Open FlightAware search + const url = `https://flightaware.com/live/flight/${searchTerm}`; + window.open(url, '_blank'); + } + + function updateFlightLookupBtn() { + const btn = document.getElementById('flightLookupBtn'); + if (selectedIcao && aircraft[selectedIcao]) { + btn.disabled = false; + const ac = aircraft[selectedIcao]; + const label = ac.callsign || ac.registration || selectedIcao; + btn.title = `Lookup ${label} on FlightAware`; + } else { + btn.disabled = true; + btn.title = 'Select an aircraft first'; + } } // ============================================ @@ -1547,6 +2145,7 @@ sudo make install if (data.status === 'success' || data.status === 'started' || data.status === 'already_running') { startEventStream(); drawRangeRings(); + startSessionTimer(); isTracking = true; adsbActiveDevice = adsbDevice; // Track which device is being used btn.textContent = 'STOP'; @@ -1704,15 +2303,16 @@ sudo make install const color = militaryInfo.military ? '#556b2f' : getAltitudeColor(ac.altitude); const callsign = ac.callsign || icao; const alt = ac.altitude ? ac.altitude + ' ft' : 'N/A'; + const iconType = getAircraftIconType(ac.type_code, militaryInfo.military); const prevState = markerState[icao] || {}; - const iconChanged = prevState.rotation !== rotation || prevState.color !== color; + const iconChanged = prevState.rotation !== rotation || prevState.color !== color || prevState.iconType !== iconType; const tooltipChanged = prevState.callsign !== callsign || prevState.alt !== alt; if (markers[icao]) { markers[icao].setLatLng([ac.lat, ac.lon]); if (iconChanged) { - markers[icao].setIcon(createMarkerIcon(rotation, color)); + markers[icao].setIcon(createMarkerIcon(rotation, color, iconType)); } if (tooltipChanged) { markers[icao].unbindTooltip(); @@ -1721,7 +2321,7 @@ sudo make install }); } } else { - markers[icao] = L.marker([ac.lat, ac.lon], { icon: createMarkerIcon(rotation, color) }) + markers[icao] = L.marker([ac.lat, ac.lon], { icon: createMarkerIcon(rotation, color, iconType) }) .addTo(radarMap) .on('click', () => selectAircraft(icao)); markers[icao].bindTooltip(`${callsign}
${alt}`, { @@ -1729,17 +2329,59 @@ sudo make install }); } - markerState[icao] = { rotation, color, callsign, alt }; + markerState[icao] = { rotation, color, callsign, alt, iconType }; } - function createMarkerIcon(rotation, color) { + // Aircraft type icon SVG paths + const AIRCRAFT_ICONS = { + jet: 'M12 2L8 10H4v2l8 4 8-4v-2h-4L12 2zm0 14l-6 3v1h12v-1l-6-3z', + helicopter: 'M12 4L10 6H8V8h1l3 8 3-8h1V6h-2L12 4zm-1 14v2H9v1h6v-1h-2v-2h-2zm7-7h-2v2h2v-2zM4 11h2v2H4v-2z', + prop: 'M12 3L9 8H5v2l7 6 7-6v-2h-4L12 3zm0 12l-4 2v1h8v-1l-4-2z', + military: 'M12 2L7 9H3l1 3 8 6 8-6 1-3h-4L12 2zm0 14l-5 2.5V20h10v-1.5L12 16z', + glider: 'M12 4L10 8H4v1.5l8 4 8-4V8h-6L12 4zm0 10l-6 2v1h12v-1l-6-2z' + }; + + // Determine aircraft type from type_code + function getAircraftIconType(typeCode, isMilitary) { + if (isMilitary) return 'military'; + if (!typeCode) return 'jet'; + + const code = typeCode.toUpperCase(); + + // Helicopters + if (code.startsWith('H') || code.includes('HELI') || + ['R22', 'R44', 'R66', 'EC35', 'EC45', 'AS50', 'AS55', 'AS65', 'B06', 'B212', 'B412', 'S76', 'A109', 'AW139', 'AW169'].some(h => code.includes(h))) { + return 'helicopter'; + } + + // Gliders + if (code.startsWith('G') || code.includes('GLID')) { + return 'glider'; + } + + // Light props (common GA aircraft) + if (['C150', 'C152', 'C172', 'C182', 'C206', 'C208', 'C210', 'PA28', 'PA32', 'PA34', 'PA44', 'PA46', 'SR20', 'SR22', 'DA40', 'DA42', 'TB20', 'M20', 'BE35', 'BE36', 'BE58'].some(p => code.includes(p))) { + return 'prop'; + } + + // Turboprops + if (['ATR', 'DH8', 'DHC', 'SF34', 'J328', 'B190', 'PC12', 'TBM'].some(t => code.includes(t))) { + return 'prop'; + } + + return 'jet'; + } + + function createMarkerIcon(rotation, color, iconType = 'jet') { + const path = AIRCRAFT_ICONS[iconType] || AIRCRAFT_ICONS.jet; + const size = iconType === 'helicopter' ? 22 : 24; return L.divIcon({ - className: 'aircraft-marker', - html: ` - + className: `aircraft-marker aircraft-${iconType}`, + html: ` + `, - iconSize: [24, 24], - iconAnchor: [12, 12] + iconSize: [size, size], + iconAnchor: [size/2, size/2] }); } @@ -1852,6 +2494,7 @@ sudo make install selectedIcao = icao; renderAircraftList(); showAircraftDetails(icao); + updateFlightLookupBtn(); const ac = aircraft[icao]; if (ac && ac.lat && ac.lon && currentView === 'map') { @@ -2008,6 +2651,7 @@ sudo make install if (selectedIcao === icao) { selectedIcao = null; showAircraftDetails(null); + updateFlightLookupBtn(); } } }); @@ -2438,7 +3082,9 @@ sudo make install const data = JSON.parse(e.data); if (data.type === 'acars') { acarsMessageCount++; + stats.acarsMessages++; document.getElementById('acarsCount').textContent = acarsMessageCount; + document.getElementById('stripAcars').textContent = stats.acarsMessages; addAcarsMessage(data); } };