diff --git a/config.example.json b/config.example.json
index eba437c..f275347 100644
--- a/config.example.json
+++ b/config.example.json
@@ -35,6 +35,11 @@
"enabled": false
}
],
+ "origin": {
+ "latitude": 51.4700,
+ "longitude": -0.4600,
+ "name": "Control Tower"
+ },
"settings": {
"history_limit": 1000,
"stale_timeout": 60,
diff --git a/internal/assets/assets.go b/internal/assets/assets.go
new file mode 100644
index 0000000..fa6422f
--- /dev/null
+++ b/internal/assets/assets.go
@@ -0,0 +1,10 @@
+// Package assets provides embedded static web assets for the SkyView application.
+// This package embeds all files from the static/ directory at build time.
+package assets
+
+import "embed"
+
+// Static contains all embedded static assets
+// The files are accessed with paths like "static/index.html", "static/css/style.css", etc.
+//go:embed static/*
+var Static embed.FS
\ No newline at end of file
diff --git a/internal/assets/static/aircraft-icon.svg b/internal/assets/static/aircraft-icon.svg
new file mode 100644
index 0000000..f2489d3
--- /dev/null
+++ b/internal/assets/static/aircraft-icon.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/internal/assets/static/css/style.css b/internal/assets/static/css/style.css
new file mode 100644
index 0000000..0f2e125
--- /dev/null
+++ b/internal/assets/static/css/style.css
@@ -0,0 +1,488 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: #1a1a1a;
+ color: #ffffff;
+ height: 100vh;
+ overflow: hidden;
+}
+
+#app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem 2rem;
+ background: #2d2d2d;
+ border-bottom: 1px solid #404040;
+}
+
+.clock-section {
+ flex: 1;
+ display: flex;
+ justify-content: center;
+}
+
+.clock-display {
+ display: flex;
+ gap: 2rem;
+}
+
+.clock {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.clock-face {
+ position: relative;
+ width: 60px;
+ height: 60px;
+ border: 2px solid #00a8ff;
+ border-radius: 50%;
+ background: #1a1a1a;
+}
+
+.clock-face::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 4px;
+ height: 4px;
+ background: #00a8ff;
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 3;
+}
+
+.clock-hand {
+ position: absolute;
+ background: #00a8ff;
+ transform-origin: bottom center;
+ border-radius: 2px;
+}
+
+.hour-hand {
+ width: 3px;
+ height: 18px;
+ top: 12px;
+ left: 50%;
+ margin-left: -1.5px;
+}
+
+.minute-hand {
+ width: 2px;
+ height: 25px;
+ top: 5px;
+ left: 50%;
+ margin-left: -1px;
+}
+
+.clock-label {
+ font-size: 0.8rem;
+ color: #888;
+ text-align: center;
+}
+
+.header h1 {
+ font-size: 1.5rem;
+ color: #00a8ff;
+}
+
+.stats-summary {
+ display: flex;
+ gap: 1rem;
+ font-size: 0.9rem;
+}
+
+.connection-status {
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.8rem;
+}
+
+.connection-status.connected {
+ background: #27ae60;
+}
+
+.connection-status.disconnected {
+ background: #e74c3c;
+}
+
+.main-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.view-toggle {
+ display: flex;
+ background: #2d2d2d;
+ border-bottom: 1px solid #404040;
+}
+
+.view-btn {
+ padding: 0.75rem 1.5rem;
+ background: transparent;
+ border: none;
+ color: #ffffff;
+ cursor: pointer;
+ border-bottom: 3px solid transparent;
+ transition: all 0.2s ease;
+}
+
+.view-btn:hover {
+ background: #404040;
+}
+
+.view-btn.active {
+ border-bottom-color: #00a8ff;
+ background: #404040;
+}
+
+.view {
+ flex: 1;
+ display: none;
+ overflow: hidden;
+}
+
+.view.active {
+ display: flex;
+ flex-direction: column;
+}
+
+#map {
+ flex: 1;
+ z-index: 1;
+}
+
+.map-controls {
+ position: absolute;
+ top: 80px;
+ right: 10px;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.map-controls button {
+ padding: 0.5rem 1rem;
+ background: #2d2d2d;
+ border: 1px solid #404040;
+ color: #ffffff;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.map-controls button:hover {
+ background: #404040;
+}
+
+.legend {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+ background: rgba(45, 45, 45, 0.95);
+ border: 1px solid #404040;
+ border-radius: 8px;
+ padding: 1rem;
+ z-index: 1000;
+ min-width: 150px;
+}
+
+.legend h4 {
+ margin: 0 0 0.5rem 0;
+ font-size: 0.9rem;
+ color: #ffffff;
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.25rem;
+ font-size: 0.8rem;
+}
+
+.legend-icon {
+ width: 16px;
+ height: 16px;
+ border-radius: 2px;
+ border: 1px solid #ffffff;
+}
+
+.legend-icon.commercial { background: #00ff88; }
+.legend-icon.cargo { background: #ff8c00; }
+.legend-icon.military { background: #ff4444; }
+.legend-icon.ga { background: #ffff00; }
+.legend-icon.ground { background: #888888; }
+
+.table-controls {
+ display: flex;
+ gap: 1rem;
+ padding: 1rem;
+ background: #2d2d2d;
+ border-bottom: 1px solid #404040;
+}
+
+.table-controls input,
+.table-controls select {
+ padding: 0.5rem;
+ background: #404040;
+ border: 1px solid #606060;
+ color: #ffffff;
+ border-radius: 4px;
+}
+
+.table-controls input {
+ flex: 1;
+}
+
+.table-container {
+ flex: 1;
+ overflow: auto;
+}
+
+#aircraft-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+#aircraft-table th,
+#aircraft-table td {
+ padding: 0.75rem;
+ text-align: left;
+ border-bottom: 1px solid #404040;
+}
+
+#aircraft-table th {
+ background: #2d2d2d;
+ font-weight: 600;
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+#aircraft-table tr:hover {
+ background: #404040;
+}
+
+.type-badge {
+ padding: 0.2rem 0.4rem;
+ border-radius: 3px;
+ font-size: 0.7rem;
+ font-weight: bold;
+ color: #000000;
+}
+
+.type-badge.commercial { background: #00ff88; }
+.type-badge.cargo { background: #ff8c00; }
+.type-badge.military { background: #ff4444; }
+.type-badge.ga { background: #ffff00; }
+.type-badge.ground { background: #888888; color: #ffffff; }
+
+/* RSSI signal strength colors */
+.rssi-strong { color: #00ff88; }
+.rssi-good { color: #ffff00; }
+.rssi-weak { color: #ff8c00; }
+.rssi-poor { color: #ff4444; }
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ padding: 1rem;
+}
+
+.stat-card {
+ background: #2d2d2d;
+ padding: 1.5rem;
+ border-radius: 8px;
+ border: 1px solid #404040;
+ text-align: center;
+}
+
+.stat-card h3 {
+ font-size: 0.9rem;
+ color: #888;
+ margin-bottom: 0.5rem;
+}
+
+.stat-value {
+ font-size: 2rem;
+ font-weight: bold;
+ color: #00a8ff;
+}
+
+.charts-container {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+ padding: 1rem;
+ flex: 1;
+}
+
+.chart-card {
+ background: #2d2d2d;
+ padding: 1rem;
+ border-radius: 8px;
+ border: 1px solid #404040;
+ display: flex;
+ flex-direction: column;
+}
+
+.chart-card h3 {
+ margin-bottom: 1rem;
+ color: #888;
+}
+
+.chart-card canvas {
+ flex: 1;
+ max-height: 300px;
+}
+
+.aircraft-marker {
+ transform: rotate(0deg);
+ filter: drop-shadow(0 0 4px rgba(0,0,0,0.9));
+ z-index: 1000;
+}
+
+.aircraft-popup {
+ min-width: 300px;
+ max-width: 400px;
+}
+
+.popup-header {
+ border-bottom: 1px solid #404040;
+ padding-bottom: 0.5rem;
+ margin-bottom: 0.75rem;
+}
+
+.flight-info {
+ font-size: 1.1rem;
+ font-weight: bold;
+}
+
+.icao-flag {
+ font-size: 1.2rem;
+ margin-right: 0.5rem;
+}
+
+.flight-id {
+ color: #00a8ff;
+ font-family: monospace;
+}
+
+.callsign {
+ color: #00ff88;
+}
+
+.popup-details {
+ font-size: 0.9rem;
+}
+
+.detail-row {
+ margin-bottom: 0.5rem;
+ padding: 0.25rem 0;
+}
+
+.detail-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.5rem;
+ margin: 0.75rem 0;
+}
+
+.detail-item {
+ display: flex;
+ flex-direction: column;
+}
+
+.detail-item .label {
+ font-size: 0.8rem;
+ color: #888;
+ margin-bottom: 0.1rem;
+}
+
+.detail-item .value {
+ font-weight: bold;
+ color: #ffffff;
+}
+
+@media (max-width: 768px) {
+ .header {
+ padding: 0.75rem 1rem;
+ }
+
+ .header h1 {
+ font-size: 1.25rem;
+ }
+
+ .stats-summary {
+ font-size: 0.8rem;
+ gap: 0.5rem;
+ }
+
+ .table-controls {
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+
+ .charts-container {
+ grid-template-columns: 1fr;
+ }
+
+ .stats-grid {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0.5rem;
+ padding: 0.5rem;
+ }
+
+ .stat-card {
+ padding: 1rem;
+ }
+
+ .stat-value {
+ font-size: 1.5rem;
+ }
+
+ .map-controls {
+ top: 70px;
+ right: 5px;
+ }
+
+ .map-controls button {
+ padding: 0.4rem 0.8rem;
+ font-size: 0.8rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .stats-grid {
+ grid-template-columns: 1fr;
+ }
+
+ #aircraft-table {
+ font-size: 0.8rem;
+ }
+
+ #aircraft-table th,
+ #aircraft-table td {
+ padding: 0.5rem 0.25rem;
+ }
+}
\ No newline at end of file
diff --git a/internal/assets/static/favicon.ico b/internal/assets/static/favicon.ico
new file mode 100644
index 0000000..a0f2003
--- /dev/null
+++ b/internal/assets/static/favicon.ico
@@ -0,0 +1 @@
+data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
\ No newline at end of file
diff --git a/internal/assets/static/index.html b/internal/assets/static/index.html
new file mode 100644
index 0000000..5dd05b6
--- /dev/null
+++ b/internal/assets/static/index.html
@@ -0,0 +1,226 @@
+
+
+
+
+
+ SkyView - Multi-Source ADS-B Aircraft Tracker
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Aircraft Types
+
+
+ Commercial
+
+
+
+ Cargo
+
+
+
+ Military
+
+
+
+ General Aviation
+
+
+
+ Ground
+
+
+
Sources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | ICAO |
+ Flight |
+ Squawk |
+ Altitude |
+ Speed |
+ Distance |
+ Track |
+ Sources |
+ Signal |
+ Age |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Aircraft Count Timeline
+
+
+
+
Message Rate by Source
+
+
+
+
Signal Strength Distribution
+
+
+
+
Altitude Distribution
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/internal/assets/static/js/app.js b/internal/assets/static/js/app.js
new file mode 100644
index 0000000..0ca2c8d
--- /dev/null
+++ b/internal/assets/static/js/app.js
@@ -0,0 +1,1151 @@
+// Import Three.js modules
+import * as THREE from 'three';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+class SkyView {
+ constructor() {
+ this.map = null;
+ this.coverageMap = null;
+ this.radar3d = null;
+ this.websocket = null;
+
+ // Data storage
+ this.aircraftData = new Map();
+ this.sourcesData = new Map();
+ this.stats = {};
+
+ // Map markers and overlays
+ this.aircraftMarkers = new Map();
+ this.sourceMarkers = new Map();
+ this.aircraftTrails = new Map();
+ this.rangeCircles = new Map();
+ this.heatmapLayer = null;
+
+ // UI state
+ this.currentView = 'map-view';
+ this.showTrails = false;
+ this.showRange = false;
+ this.showSources = true;
+ this.selectedSource = null;
+
+ // Charts
+ this.charts = {};
+
+ // Update tracking
+ this.lastUpdateTime = new Date();
+
+ this.init();
+ }
+
+ async init() {
+ try {
+ this.initializeViews();
+ this.initializeMap();
+ await this.initializeWebSocket();
+ this.initializeEventListeners();
+ this.initializeCharts();
+ this.initializeClocks();
+ this.initialize3DRadar();
+
+ this.startPeriodicTasks();
+ } catch (error) {
+ console.error('Initialization failed:', error);
+ this.showError('Failed to initialize application');
+ }
+ }
+
+ // View Management
+ initializeViews() {
+ const viewButtons = document.querySelectorAll('.view-btn');
+ const views = document.querySelectorAll('.view');
+
+ viewButtons.forEach(btn => {
+ btn.addEventListener('click', () => {
+ const viewId = btn.id.replace('-btn', '');
+ this.switchView(viewId);
+ });
+ });
+ }
+
+ switchView(viewId) {
+ // Update buttons
+ document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active'));
+ const activeBtn = document.getElementById(`${viewId}-btn`);
+ if (activeBtn) {
+ activeBtn.classList.add('active');
+ }
+
+ // Update views (viewId already includes the full view ID like "map-view")
+ document.querySelectorAll('.view').forEach(view => view.classList.remove('active'));
+ const activeView = document.getElementById(viewId);
+ if (activeView) {
+ activeView.classList.add('active');
+ } else {
+ console.warn(`View element not found: ${viewId}`);
+ return;
+ }
+
+ this.currentView = viewId;
+
+ // Handle view-specific initialization (extract base name for switch)
+ const baseName = viewId.replace('-view', '');
+ switch (baseName) {
+ case 'coverage':
+ this.initializeCoverageMap();
+ break;
+ case 'radar3d':
+ this.update3DRadar();
+ break;
+ }
+ }
+
+ // Map Initialization
+ initializeMap() {
+ this.map = L.map('map').setView([51.4700, -0.4600], 10);
+
+ // Dark tile layer
+ L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
+ attribution: '© OpenStreetMap contributors © CARTO',
+ subdomains: 'abcd',
+ maxZoom: 19
+ }).addTo(this.map);
+
+ // Map controls
+ document.getElementById('center-map').addEventListener('click', () => this.centerMapOnAircraft());
+ document.getElementById('toggle-trails').addEventListener('click', () => this.toggleTrails());
+ document.getElementById('toggle-range').addEventListener('click', () => this.toggleRangeCircles());
+ document.getElementById('toggle-sources').addEventListener('click', () => this.toggleSources());
+ }
+
+ initializeCoverageMap() {
+ if (!this.coverageMap) {
+ this.coverageMap = L.map('coverage-map').setView([51.4700, -0.4600], 10);
+
+ L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
+ attribution: '© OpenStreetMap contributors'
+ }).addTo(this.coverageMap);
+ }
+
+ // Update coverage controls
+ this.updateCoverageControls();
+
+ // Coverage controls
+ document.getElementById('toggle-heatmap').addEventListener('click', () => this.toggleHeatmap());
+ document.getElementById('coverage-source').addEventListener('change', (e) => {
+ this.selectedSource = e.target.value;
+ this.updateCoverageDisplay();
+ });
+ }
+
+ // WebSocket Connection
+ async initializeWebSocket() {
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const wsUrl = `${protocol}//${window.location.host}/ws`;
+
+ try {
+ this.websocket = new WebSocket(wsUrl);
+
+ this.websocket.onopen = () => {
+ console.log('WebSocket connected');
+ this.updateConnectionStatus('connected');
+ };
+
+ this.websocket.onclose = () => {
+ console.log('WebSocket disconnected');
+ this.updateConnectionStatus('disconnected');
+ // Reconnect after 5 seconds
+ setTimeout(() => this.initializeWebSocket(), 5000);
+ };
+
+ this.websocket.onerror = (error) => {
+ console.error('WebSocket error:', error);
+ this.updateConnectionStatus('disconnected');
+ };
+
+ this.websocket.onmessage = (event) => {
+ try {
+ const message = JSON.parse(event.data);
+ this.handleWebSocketMessage(message);
+ } catch (error) {
+ console.error('Failed to parse WebSocket message:', error);
+ }
+ };
+
+ } catch (error) {
+ console.error('WebSocket connection failed:', error);
+ this.updateConnectionStatus('disconnected');
+ }
+ }
+
+ handleWebSocketMessage(message) {
+ switch (message.type) {
+ case 'initial_data':
+ case 'aircraft_update':
+ this.updateData(message.data);
+ break;
+ default:
+ console.log('Unknown message type:', message.type);
+ }
+ }
+
+ updateData(data) {
+ // Update aircraft data
+ if (data.aircraft) {
+ this.aircraftData.clear();
+ for (const [icao, aircraft] of Object.entries(data.aircraft)) {
+ this.aircraftData.set(icao, aircraft);
+ }
+ }
+
+ // Update sources data
+ if (data.sources) {
+ this.sourcesData.clear();
+ data.sources.forEach(source => {
+ this.sourcesData.set(source.id, source);
+ });
+ }
+
+ // Update statistics
+ if (data.stats) {
+ this.stats = data.stats;
+ }
+
+ this.lastUpdateTime = new Date();
+
+ // Update UI
+ this.updateMapMarkers();
+ this.updateAircraftTable();
+ this.updateStatistics();
+ this.updateHeaderInfo();
+
+ if (this.currentView === 'radar3d-view') {
+ this.update3DRadar();
+ }
+ }
+
+ // Map Updates
+ updateMapMarkers() {
+ // Clear stale aircraft markers
+ const currentICAOs = new Set(this.aircraftData.keys());
+ for (const [icao, marker] of this.aircraftMarkers) {
+ if (!currentICAOs.has(icao)) {
+ this.map.removeLayer(marker);
+ this.aircraftMarkers.delete(icao);
+ this.aircraftTrails.delete(icao);
+ }
+ }
+
+ // Update aircraft markers
+ for (const [icao, aircraft] of this.aircraftData) {
+ if (aircraft.Latitude && aircraft.Longitude) {
+ this.updateAircraftMarker(icao, aircraft);
+ }
+ }
+
+ // Update source markers
+ if (this.showSources) {
+ this.updateSourceMarkers();
+ }
+
+ // Update range circles
+ if (this.showRange) {
+ this.updateRangeCircles();
+ }
+ }
+
+ updateAircraftMarker(icao, aircraft) {
+ const pos = [aircraft.Latitude, aircraft.Longitude];
+
+ if (this.aircraftMarkers.has(icao)) {
+ // Update existing marker
+ const marker = this.aircraftMarkers.get(icao);
+ marker.setLatLng(pos);
+ this.updateMarkerRotation(marker, aircraft.Track || 0);
+ this.updatePopupContent(marker, aircraft);
+ } else {
+ // Create new marker
+ const icon = this.createAircraftIcon(aircraft);
+ const marker = L.marker(pos, {
+ icon: icon,
+ rotationAngle: aircraft.Track || 0
+ }).addTo(this.map);
+
+ marker.bindPopup(this.createPopupContent(aircraft), {
+ maxWidth: 450,
+ className: 'aircraft-popup'
+ });
+
+ this.aircraftMarkers.set(icao, marker);
+ }
+
+ // Update trails
+ if (this.showTrails) {
+ this.updateAircraftTrail(icao, pos);
+ }
+ }
+
+ createAircraftIcon(aircraft) {
+ const type = this.getAircraftType(aircraft);
+ const color = this.getAircraftColor(type);
+ const size = aircraft.OnGround ? 12 : 16;
+
+ const svg = `
+
+ `;
+
+ return L.divIcon({
+ html: svg,
+ iconSize: [size * 2, size * 2],
+ iconAnchor: [size, size],
+ className: 'aircraft-marker'
+ });
+ }
+
+ getAircraftType(aircraft) {
+ if (aircraft.OnGround) return 'ground';
+ if (aircraft.Category) {
+ const cat = aircraft.Category.toLowerCase();
+ if (cat.includes('military')) return 'military';
+ if (cat.includes('cargo') || cat.includes('heavy')) return 'cargo';
+ if (cat.includes('light') || cat.includes('glider')) return 'ga';
+ }
+ if (aircraft.Callsign) {
+ const cs = aircraft.Callsign.toLowerCase();
+ if (cs.includes('mil') || cs.includes('army') || cs.includes('navy')) return 'military';
+ if (cs.includes('cargo') || cs.includes('fedex') || cs.includes('ups')) return 'cargo';
+ }
+ return 'commercial';
+ }
+
+ getAircraftColor(type) {
+ const colors = {
+ commercial: '#00ff88',
+ cargo: '#ff8c00',
+ military: '#ff4444',
+ ga: '#ffff00',
+ ground: '#888888'
+ };
+ return colors[type] || colors.commercial;
+ }
+
+ updateMarkerRotation(marker, track) {
+ if (marker._icon) {
+ marker._icon.style.transform = `rotate(${track}deg)`;
+ }
+ }
+
+ updateSourceMarkers() {
+ // Remove markers for sources that no longer exist
+ const currentSourceIds = new Set(this.sourcesData.keys());
+ for (const [id, marker] of this.sourceMarkers) {
+ if (!currentSourceIds.has(id)) {
+ this.map.removeLayer(marker);
+ this.sourceMarkers.delete(id);
+ }
+ }
+
+ // Update or create markers for current sources
+ for (const [id, source] of this.sourcesData) {
+ if (source.latitude && source.longitude) {
+ if (this.sourceMarkers.has(id)) {
+ // Update existing marker
+ const marker = this.sourceMarkers.get(id);
+
+ // Update marker style if status changed
+ marker.setStyle({
+ radius: source.active ? 10 : 6,
+ fillColor: source.active ? '#00d4ff' : '#666666',
+ fillOpacity: 0.8
+ });
+
+ // Update popup content if it's open
+ if (marker.isPopupOpen()) {
+ marker.setPopupContent(this.createSourcePopupContent(source));
+ }
+ } else {
+ // Create new marker
+ const marker = L.circleMarker([source.latitude, source.longitude], {
+ radius: source.active ? 10 : 6,
+ fillColor: source.active ? '#00d4ff' : '#666666',
+ color: '#ffffff',
+ weight: 2,
+ fillOpacity: 0.8,
+ className: 'source-marker'
+ }).addTo(this.map);
+
+ marker.bindPopup(this.createSourcePopupContent(source), {
+ maxWidth: 300
+ });
+
+ this.sourceMarkers.set(id, marker);
+ }
+ }
+ }
+
+ this.updateSourcesLegend();
+ }
+
+ updateRangeCircles() {
+ // Clear existing circles
+ this.rangeCircles.forEach(circle => this.map.removeLayer(circle));
+ this.rangeCircles.clear();
+
+ // Add range circles for active sources
+ for (const [id, source] of this.sourcesData) {
+ if (source.active && source.latitude && source.longitude) {
+ // Add multiple range circles (50km, 100km, 200km)
+ const ranges = [50000, 100000, 200000];
+ ranges.forEach((range, index) => {
+ const circle = L.circle([source.latitude, source.longitude], {
+ radius: range,
+ fillColor: 'transparent',
+ color: '#00d4ff',
+ weight: 1,
+ opacity: 0.3 - (index * 0.1),
+ dashArray: '5,5'
+ }).addTo(this.map);
+
+ this.rangeCircles.set(`${id}_${range}`, circle);
+ });
+ }
+ }
+ }
+
+ updateAircraftTrail(icao, pos) {
+ if (!this.aircraftTrails.has(icao)) {
+ this.aircraftTrails.set(icao, []);
+ }
+
+ const trail = this.aircraftTrails.get(icao);
+ trail.push(pos);
+
+ // Keep only last 50 positions
+ if (trail.length > 50) {
+ trail.shift();
+ }
+
+ // Draw polyline
+ const trailLine = L.polyline(trail, {
+ color: '#00d4ff',
+ weight: 2,
+ opacity: 0.6
+ }).addTo(this.map);
+
+ // Store reference for cleanup
+ if (!this.aircraftTrails.get(icao).polyline) {
+ this.aircraftTrails.get(icao).polyline = trailLine;
+ } else {
+ this.map.removeLayer(this.aircraftTrails.get(icao).polyline);
+ this.aircraftTrails.get(icao).polyline = trailLine;
+ }
+ }
+
+ // Popup Content
+ createPopupContent(aircraft) {
+ const type = this.getAircraftType(aircraft);
+ const country = this.getCountryFromICAO(aircraft.ICAO24 ? aircraft.ICAO24.toString(16).toUpperCase() : '');
+ const flag = this.getCountryFlag(country);
+
+ const altitude = aircraft.Altitude || aircraft.BaroAltitude || 0;
+ const altitudeM = altitude ? Math.round(altitude * 0.3048) : 0;
+ const speedKmh = aircraft.GroundSpeed ? Math.round(aircraft.GroundSpeed * 1.852) : 0;
+ const distance = this.calculateDistance(aircraft);
+ const distanceKm = distance ? (distance * 1.852).toFixed(1) : 'N/A';
+
+ const sources = aircraft.Sources ? Object.keys(aircraft.Sources).map(id => {
+ const source = this.sourcesData.get(id);
+ const srcData = aircraft.Sources[id];
+ return `
+ ${source?.name || id}
+ `;
+ }).join('') : 'N/A';
+
+ return `
+
+ `;
+ }
+
+ updatePopupContent(marker, aircraft) {
+ if (marker && marker.isPopupOpen()) {
+ const newContent = this.createPopupContent(aircraft);
+ marker.setPopupContent(newContent);
+ }
+ }
+
+ createSourcePopupContent(source) {
+ const aircraftCount = Array.from(this.aircraftData.values())
+ .filter(aircraft => aircraft.Sources && aircraft.Sources[source.id]).length;
+
+ return `
+
+ `;
+ }
+
+ updateSourcesLegend() {
+ const legend = document.getElementById('sources-legend');
+ legend.innerHTML = '';
+
+ for (const [id, source] of this.sourcesData) {
+ const item = document.createElement('div');
+ item.className = 'legend-item';
+ item.innerHTML = `
+
+ ${source.name}
+ `;
+ legend.appendChild(item);
+ }
+ }
+
+ // Table Management
+ updateAircraftTable() {
+ const tbody = document.getElementById('aircraft-tbody');
+ tbody.innerHTML = '';
+
+ let filteredData = Array.from(this.aircraftData.values());
+
+ // Apply filters
+ const searchTerm = document.getElementById('search-input').value.toLowerCase();
+ const sourceFilter = document.getElementById('source-filter').value;
+
+ if (searchTerm) {
+ filteredData = filteredData.filter(aircraft =>
+ (aircraft.Callsign && aircraft.Callsign.toLowerCase().includes(searchTerm)) ||
+ (aircraft.ICAO24 && aircraft.ICAO24.toString(16).toLowerCase().includes(searchTerm)) ||
+ (aircraft.Squawk && aircraft.Squawk.includes(searchTerm))
+ );
+ }
+
+ if (sourceFilter) {
+ filteredData = filteredData.filter(aircraft =>
+ aircraft.Sources && aircraft.Sources[sourceFilter]
+ );
+ }
+
+ // Sort data
+ const sortBy = document.getElementById('sort-select').value;
+ this.sortAircraft(filteredData, sortBy);
+
+ // Populate table
+ filteredData.forEach(aircraft => {
+ const row = this.createTableRow(aircraft);
+ tbody.appendChild(row);
+ });
+
+ // Update source filter options
+ this.updateSourceFilter();
+ }
+
+ createTableRow(aircraft) {
+ const type = this.getAircraftType(aircraft);
+ const icao = aircraft.ICAO24 ? aircraft.ICAO24.toString(16).toUpperCase() : 'N/A';
+ const altitude = aircraft.Altitude || aircraft.BaroAltitude || 0;
+ const distance = this.calculateDistance(aircraft);
+ const sources = aircraft.Sources ? Object.keys(aircraft.Sources).length : 0;
+ const bestSignal = this.getBestSignalFromSources(aircraft.Sources);
+
+ const row = document.createElement('tr');
+ row.innerHTML = `
+ ${icao} |
+ ${aircraft.Callsign || '-'} |
+ ${aircraft.Squawk || '-'} |
+ ${altitude ? `${altitude} ft` : '-'} |
+ ${aircraft.GroundSpeed || '-'} kt |
+ ${distance ? distance.toFixed(1) : '-'} km |
+ ${aircraft.Track || '-'}° |
+ ${sources} |
+ ${bestSignal ? bestSignal.toFixed(1) : '-'} |
+ ${aircraft.Age ? aircraft.Age.toFixed(0) : '0'}s |
+ `;
+
+ row.addEventListener('click', () => {
+ if (aircraft.Latitude && aircraft.Longitude) {
+ this.switchView('map');
+ this.map.setView([aircraft.Latitude, aircraft.Longitude], 12);
+ const marker = this.aircraftMarkers.get(icao);
+ if (marker) {
+ marker.openPopup();
+ }
+ }
+ });
+
+ return row;
+ }
+
+ getBestSignalFromSources(sources) {
+ if (!sources) return null;
+ let bestSignal = -999;
+ for (const [id, data] of Object.entries(sources)) {
+ if (data.SignalLevel > bestSignal) {
+ bestSignal = data.SignalLevel;
+ }
+ }
+ return bestSignal === -999 ? null : bestSignal;
+ }
+
+ getSignalClass(signal) {
+ if (!signal) return '';
+ if (signal > -10) return 'signal-strong';
+ if (signal > -20) return 'signal-good';
+ if (signal > -30) return 'signal-weak';
+ return 'signal-poor';
+ }
+
+ updateSourceFilter() {
+ const select = document.getElementById('source-filter');
+ const currentValue = select.value;
+
+ // Clear options except "All Sources"
+ select.innerHTML = '';
+
+ // Add source options
+ for (const [id, source] of this.sourcesData) {
+ const option = document.createElement('option');
+ option.value = id;
+ option.textContent = source.name;
+ if (id === currentValue) option.selected = true;
+ select.appendChild(option);
+ }
+ }
+
+ sortAircraft(aircraft, sortBy) {
+ aircraft.sort((a, b) => {
+ switch (sortBy) {
+ case 'distance':
+ return (this.calculateDistance(a) || Infinity) - (this.calculateDistance(b) || Infinity);
+ case 'altitude':
+ return (b.Altitude || b.BaroAltitude || 0) - (a.Altitude || a.BaroAltitude || 0);
+ case 'speed':
+ return (b.GroundSpeed || 0) - (a.GroundSpeed || 0);
+ case 'flight':
+ return (a.Callsign || a.ICAO24?.toString(16) || '').localeCompare(b.Callsign || b.ICAO24?.toString(16) || '');
+ case 'icao':
+ return (a.ICAO24?.toString(16) || '').localeCompare(b.ICAO24?.toString(16) || '');
+ case 'squawk':
+ return (a.Squawk || '').localeCompare(b.Squawk || '');
+ case 'signal':
+ return (this.getBestSignalFromSources(b.Sources) || -999) - (this.getBestSignalFromSources(a.Sources) || -999);
+ case 'age':
+ return (a.Age || 0) - (b.Age || 0);
+ default:
+ return 0;
+ }
+ });
+ }
+
+ // Statistics
+ updateStatistics() {
+ document.getElementById('total-aircraft').textContent = this.aircraftData.size;
+ document.getElementById('active-sources').textContent =
+ Array.from(this.sourcesData.values()).filter(s => s.active).length;
+
+ // Calculate max range
+ let maxDistance = 0;
+ for (const aircraft of this.aircraftData.values()) {
+ const distance = this.calculateDistance(aircraft);
+ if (distance && distance > maxDistance) {
+ maxDistance = distance;
+ }
+ }
+ document.getElementById('max-range').textContent = `${maxDistance.toFixed(1)} km`;
+
+ // Update message rate
+ const totalMessages = this.stats.total_messages || 0;
+ document.getElementById('messages-sec').textContent = Math.round(totalMessages / 60);
+
+ this.updateCharts();
+ }
+
+ updateHeaderInfo() {
+ document.getElementById('aircraft-count').textContent = `${this.aircraftData.size} aircraft`;
+ document.getElementById('sources-count').textContent = `${this.sourcesData.size} sources`;
+
+ this.updateClocks();
+ }
+
+ // Event Listeners
+ initializeEventListeners() {
+ document.getElementById('search-input').addEventListener('input', () => this.updateAircraftTable());
+ document.getElementById('sort-select').addEventListener('change', () => this.updateAircraftTable());
+ document.getElementById('source-filter').addEventListener('change', () => this.updateAircraftTable());
+ }
+
+ // Map Controls
+ centerMapOnAircraft() {
+ if (this.aircraftData.size === 0) return;
+
+ const validAircraft = Array.from(this.aircraftData.values())
+ .filter(a => a.Latitude && a.Longitude);
+
+ if (validAircraft.length === 0) return;
+
+ const group = new L.featureGroup(
+ validAircraft.map(a => L.marker([a.Latitude, a.Longitude]))
+ );
+
+ this.map.fitBounds(group.getBounds().pad(0.1));
+ }
+
+ toggleTrails() {
+ this.showTrails = !this.showTrails;
+ document.getElementById('toggle-trails').textContent =
+ this.showTrails ? 'Hide Trails' : 'Show Trails';
+
+ if (!this.showTrails) {
+ // Clear all trails
+ this.aircraftTrails.forEach((trail, icao) => {
+ if (trail.polyline) {
+ this.map.removeLayer(trail.polyline);
+ }
+ });
+ this.aircraftTrails.clear();
+ }
+ }
+
+ toggleRangeCircles() {
+ this.showRange = !this.showRange;
+ document.getElementById('toggle-range').textContent =
+ this.showRange ? 'Hide Range' : 'Show Range';
+
+ if (this.showRange) {
+ this.updateRangeCircles();
+ } else {
+ this.rangeCircles.forEach(circle => this.map.removeLayer(circle));
+ this.rangeCircles.clear();
+ }
+ }
+
+ toggleSources() {
+ this.showSources = !this.showSources;
+ document.getElementById('toggle-sources').textContent =
+ this.showSources ? 'Hide Sources' : 'Show Sources';
+
+ if (this.showSources) {
+ this.updateSourceMarkers();
+ } else {
+ this.sourceMarkers.forEach(marker => this.map.removeLayer(marker));
+ this.sourceMarkers.clear();
+ }
+ }
+
+ // Coverage and Heatmap
+ updateCoverageControls() {
+ const select = document.getElementById('coverage-source');
+ select.innerHTML = '';
+
+ for (const [id, source] of this.sourcesData) {
+ const option = document.createElement('option');
+ option.value = id;
+ option.textContent = source.name;
+ select.appendChild(option);
+ }
+ }
+
+ async updateCoverageDisplay() {
+ if (!this.selectedSource) return;
+
+ try {
+ const response = await fetch(`/api/coverage/${this.selectedSource}`);
+ const data = await response.json();
+
+ // Clear existing coverage markers
+ this.coverageMap.eachLayer(layer => {
+ if (layer instanceof L.CircleMarker) {
+ this.coverageMap.removeLayer(layer);
+ }
+ });
+
+ // Add coverage points
+ data.points.forEach(point => {
+ const intensity = Math.max(0, (point.signal + 50) / 50); // Normalize signal strength
+ L.circleMarker([point.lat, point.lon], {
+ radius: 3,
+ fillColor: this.getSignalColor(point.signal),
+ color: 'white',
+ weight: 1,
+ fillOpacity: intensity
+ }).addTo(this.coverageMap);
+ });
+
+ } catch (error) {
+ console.error('Failed to load coverage data:', error);
+ }
+ }
+
+ async toggleHeatmap() {
+ if (!this.selectedSource) {
+ alert('Please select a source first');
+ return;
+ }
+
+ if (this.heatmapLayer) {
+ this.coverageMap.removeLayer(this.heatmapLayer);
+ this.heatmapLayer = null;
+ document.getElementById('toggle-heatmap').textContent = 'Show Heatmap';
+ } else {
+ try {
+ const response = await fetch(`/api/heatmap/${this.selectedSource}`);
+ const data = await response.json();
+
+ // Create heatmap layer (simplified)
+ // In a real implementation, you'd use a proper heatmap library
+ this.createHeatmapOverlay(data);
+ document.getElementById('toggle-heatmap').textContent = 'Hide Heatmap';
+
+ } catch (error) {
+ console.error('Failed to load heatmap data:', error);
+ }
+ }
+ }
+
+ getSignalColor(signal) {
+ if (signal > -10) return '#00ff88';
+ if (signal > -20) return '#ffff00';
+ if (signal > -30) return '#ff8c00';
+ return '#ff4444';
+ }
+
+ // Charts (continued in next part due to length)
+ initializeCharts() {
+ this.charts.aircraft = new Chart(document.getElementById('aircraft-chart'), {
+ type: 'line',
+ data: {
+ labels: [],
+ datasets: [{
+ label: 'Aircraft Count',
+ data: [],
+ borderColor: '#00d4ff',
+ backgroundColor: 'rgba(0, 212, 255, 0.1)',
+ tension: 0.4
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false }
+ },
+ scales: {
+ x: { display: false },
+ y: {
+ beginAtZero: true,
+ ticks: { color: '#888' }
+ }
+ }
+ }
+ });
+
+ // Add other charts...
+ }
+
+ updateCharts() {
+ const now = new Date();
+ const timeLabel = now.toLocaleTimeString();
+
+ // Update aircraft count chart
+ if (this.charts.aircraft) {
+ const chart = this.charts.aircraft;
+ chart.data.labels.push(timeLabel);
+ chart.data.datasets[0].data.push(this.aircraftData.size);
+
+ if (chart.data.labels.length > 20) {
+ chart.data.labels.shift();
+ chart.data.datasets[0].data.shift();
+ }
+
+ chart.update('none');
+ }
+ }
+
+ // 3D Radar (basic implementation)
+ initialize3DRadar() {
+ try {
+ const container = document.getElementById('radar3d-container');
+ if (!container) return;
+
+ // Create scene
+ this.radar3d = {
+ scene: new THREE.Scene(),
+ camera: new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000),
+ renderer: new THREE.WebGLRenderer({ alpha: true, antialias: true }),
+ controls: null,
+ aircraftMeshes: new Map()
+ };
+
+ // Set up renderer
+ this.radar3d.renderer.setSize(container.clientWidth, container.clientHeight);
+ this.radar3d.renderer.setClearColor(0x0a0a0a, 0.9);
+ container.appendChild(this.radar3d.renderer.domElement);
+
+ // Add lighting
+ const ambientLight = new THREE.AmbientLight(0x404040, 0.6);
+ this.radar3d.scene.add(ambientLight);
+
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
+ directionalLight.position.set(10, 10, 5);
+ this.radar3d.scene.add(directionalLight);
+
+ // Set up camera
+ this.radar3d.camera.position.set(0, 50, 50);
+ this.radar3d.camera.lookAt(0, 0, 0);
+
+ // Add controls
+ this.radar3d.controls = new OrbitControls(this.radar3d.camera, this.radar3d.renderer.domElement);
+ this.radar3d.controls.enableDamping = true;
+ this.radar3d.controls.dampingFactor = 0.05;
+
+ // Add ground plane
+ const groundGeometry = new THREE.PlaneGeometry(200, 200);
+ const groundMaterial = new THREE.MeshLambertMaterial({
+ color: 0x2a4d3a,
+ transparent: true,
+ opacity: 0.5
+ });
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
+ ground.rotation.x = -Math.PI / 2;
+ this.radar3d.scene.add(ground);
+
+ // Add grid
+ const gridHelper = new THREE.GridHelper(200, 20, 0x44aa44, 0x44aa44);
+ this.radar3d.scene.add(gridHelper);
+
+ // Start render loop
+ this.render3DRadar();
+
+ console.log('3D Radar initialized successfully');
+ } catch (error) {
+ console.error('Failed to initialize 3D radar:', error);
+ }
+ }
+
+ update3DRadar() {
+ if (!this.radar3d || !this.radar3d.scene) return;
+
+ try {
+ // Update aircraft positions in 3D space
+ this.aircraftData.forEach((aircraft, icao) => {
+ if (aircraft.Latitude && aircraft.Longitude) {
+ const key = icao.toString();
+
+ if (!this.radar3d.aircraftMeshes.has(key)) {
+ // Create new aircraft mesh
+ const geometry = new THREE.ConeGeometry(0.5, 2, 6);
+ const material = new THREE.MeshLambertMaterial({ color: 0x00ff00 });
+ const mesh = new THREE.Mesh(geometry, material);
+ this.radar3d.aircraftMeshes.set(key, mesh);
+ this.radar3d.scene.add(mesh);
+ }
+
+ const mesh = this.radar3d.aircraftMeshes.get(key);
+
+ // Convert lat/lon to local coordinates (simplified)
+ const x = (aircraft.Longitude - (-0.4600)) * 111320 * Math.cos(aircraft.Latitude * Math.PI / 180) / 1000;
+ const z = -(aircraft.Latitude - 51.4700) * 111320 / 1000;
+ const y = (aircraft.Altitude || 0) / 1000; // Convert feet to km for display
+
+ mesh.position.set(x, y, z);
+
+ // Orient mesh based on track
+ if (aircraft.Track !== undefined) {
+ mesh.rotation.y = -aircraft.Track * Math.PI / 180;
+ }
+ }
+ });
+
+ // Remove old aircraft
+ this.radar3d.aircraftMeshes.forEach((mesh, key) => {
+ if (!this.aircraftData.has(parseInt(key, 16))) {
+ this.radar3d.scene.remove(mesh);
+ this.radar3d.aircraftMeshes.delete(key);
+ }
+ });
+ } catch (error) {
+ console.error('Failed to update 3D radar:', error);
+ }
+ }
+
+ render3DRadar() {
+ if (!this.radar3d) return;
+
+ requestAnimationFrame(() => this.render3DRadar());
+
+ if (this.radar3d.controls) {
+ this.radar3d.controls.update();
+ }
+
+ this.radar3d.renderer.render(this.radar3d.scene, this.radar3d.camera);
+ }
+
+ // Utility functions
+ updateConnectionStatus(status) {
+ const statusEl = document.getElementById('connection-status');
+ statusEl.className = `connection-status ${status}`;
+ statusEl.textContent = status === 'connected' ? 'Connected' : 'Disconnected';
+ }
+
+ initializeClocks() {
+ this.updateClocks();
+ }
+
+ updateClocks() {
+ const now = new Date();
+ const utcNow = new Date(now.getTime() + (now.getTimezoneOffset() * 60000));
+
+ this.updateClock('utc', utcNow);
+ this.updateClock('update', this.lastUpdateTime);
+ }
+
+ updateClock(prefix, time) {
+ const hours = time.getUTCHours();
+ const minutes = time.getUTCMinutes();
+
+ const hourAngle = (hours % 12) * 30 + minutes * 0.5;
+ const minuteAngle = minutes * 6;
+
+ const hourHand = document.getElementById(`${prefix}-hour`);
+ const minuteHand = document.getElementById(`${prefix}-minute`);
+
+ if (hourHand) hourHand.style.transform = `rotate(${hourAngle}deg)`;
+ if (minuteHand) minuteHand.style.transform = `rotate(${minuteAngle}deg)`;
+ }
+
+ calculateDistance(aircraft) {
+ if (!aircraft.Latitude || !aircraft.Longitude) return null;
+
+ // Use closest source as reference point
+ let minDistance = Infinity;
+ for (const [id, srcData] of Object.entries(aircraft.Sources || {})) {
+ if (srcData.Distance && srcData.Distance < minDistance) {
+ minDistance = srcData.Distance;
+ }
+ }
+
+ return minDistance === Infinity ? null : minDistance;
+ }
+
+ getCountryFromICAO(icao) {
+ if (!icao || icao.length < 6) return 'Unknown';
+
+ const prefix = icao[0];
+ const countryMap = {
+ '4': 'Europe',
+ 'A': 'United States',
+ 'C': 'Canada',
+ 'D': 'Germany',
+ 'F': 'France',
+ 'G': 'United Kingdom',
+ 'I': 'Italy',
+ 'J': 'Japan'
+ };
+
+ return countryMap[prefix] || 'Unknown';
+ }
+
+ getCountryFlag(country) {
+ const flags = {
+ 'United States': '🇺🇸',
+ 'Canada': '🇨🇦',
+ 'Germany': '🇩🇪',
+ 'France': '🇫🇷',
+ 'United Kingdom': '🇬🇧',
+ 'Italy': '🇮🇹',
+ 'Japan': '🇯🇵',
+ 'Europe': '🇪🇺'
+ };
+ return flags[country] || '🏳️';
+ }
+
+ startPeriodicTasks() {
+ // Update clocks every second
+ setInterval(() => this.updateClocks(), 1000);
+
+ // Periodic cleanup
+ setInterval(() => {
+ // Clean up old trail data, etc.
+ }, 30000);
+ }
+
+ showError(message) {
+ console.error(message);
+ // Could implement toast notifications here
+ }
+}
+
+// Initialize application when DOM is ready
+document.addEventListener('DOMContentLoaded', () => {
+ window.skyview = new SkyView();
+});
\ No newline at end of file
diff --git a/main.go b/main.go
deleted file mode 100644
index ad52c95..0000000
--- a/main.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package main
-
-import (
- "context"
- "embed"
- "flag"
- "log"
- "net/http"
- "os"
- "os/signal"
- "syscall"
- "time"
-
- "skyview/internal/config"
- "skyview/internal/server"
-)
-
-//go:embed static/*
-var staticFiles embed.FS
-
-func main() {
- daemon := flag.Bool("daemon", false, "Run as daemon (background process)")
- flag.Parse()
-
- cfg, err := config.Load()
- if err != nil {
- log.Fatalf("Failed to load configuration: %v", err)
- }
-
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- srv := server.New(cfg, staticFiles, ctx)
-
- log.Printf("Starting skyview server on %s", cfg.Server.Address)
- log.Printf("Connecting to dump1090 SBS-1 at %s:%d", cfg.Dump1090.Host, cfg.Dump1090.DataPort)
-
- httpServer := &http.Server{
- Addr: cfg.Server.Address,
- Handler: srv,
- }
-
- go func() {
- if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
- log.Fatalf("Server failed to start: %v", err)
- }
- }()
-
- if *daemon {
- log.Printf("Running as daemon...")
- select {}
- } else {
- log.Printf("Press Ctrl+C to stop")
-
- sigChan := make(chan os.Signal, 1)
- signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
- <-sigChan
-
- log.Printf("Shutting down...")
- cancel()
-
- shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer shutdownCancel()
-
- if err := httpServer.Shutdown(shutdownCtx); err != nil {
- log.Printf("Server shutdown error: %v", err)
- }
- }
-}
\ No newline at end of file