Restructure assets to top-level package and add Reset Map button

- Move assets from internal/assets to top-level assets/ package for clean embed directive
- Consolidate all static files in single location (assets/static/)
- Remove duplicate static file locations to maintain single source of truth
- Add Reset Map button to map controls with full functionality
- Implement resetMap() method to return map to calculated origin position
- Store origin in this.mapOrigin for reset functionality
- Fix go:embed pattern to work without parent directory references

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-24 00:57:49 +02:00
commit 1425f0a018
20 changed files with 263 additions and 2139 deletions

View file

@ -6,5 +6,6 @@ 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/*
//
//go:embed static
var Static embed.FS

View file

Before

Width:  |  Height:  |  Size: 224 B

After

Width:  |  Height:  |  Size: 224 B

Before After
Before After

View file

@ -75,6 +75,7 @@
<!-- Map controls -->
<div class="map-controls">
<button id="center-map" title="Center on aircraft">Center Map</button>
<button id="reset-map" title="Reset to origin">Reset Map</button>
<button id="toggle-trails" title="Show/hide aircraft trails">Show Trails</button>
<button id="toggle-range" title="Show/hide range circles">Show Range</button>
<button id="toggle-sources" title="Show/hide source locations">Show Sources</button>

View file

@ -112,6 +112,9 @@ class SkyView {
console.warn('Could not fetch origin, using default:', error);
}
// Store origin for reset functionality
this.mapOrigin = origin;
this.map = L.map('map').setView([origin.latitude, origin.longitude], 10);
// Dark tile layer
@ -123,6 +126,7 @@ class SkyView {
// Map controls
document.getElementById('center-map').addEventListener('click', () => this.centerMapOnAircraft());
document.getElementById('reset-map').addEventListener('click', () => this.resetMap());
document.getElementById('toggle-trails').addEventListener('click', () => this.toggleTrails());
document.getElementById('toggle-range').addEventListener('click', () => this.toggleRangeCircles());
document.getElementById('toggle-sources').addEventListener('click', () => this.toggleSources());
@ -775,11 +779,23 @@ class SkyView {
if (validAircraft.length === 0) return;
const group = new L.featureGroup(
validAircraft.map(a => L.marker([a.Latitude, a.Longitude]))
if (validAircraft.length === 1) {
// Center on single aircraft
const aircraft = validAircraft[0];
this.map.setView([aircraft.Latitude, aircraft.Longitude], 12);
} else {
// Fit bounds to all aircraft
const bounds = L.latLngBounds(
validAircraft.map(a => [a.Latitude, a.Longitude])
);
this.map.fitBounds(bounds.pad(0.1));
}
}
this.map.fitBounds(group.getBounds().pad(0.1));
resetMap() {
if (this.mapOrigin && this.map) {
this.map.setView([this.mapOrigin.latitude, this.mapOrigin.longitude], 10);
}
}
toggleTrails() {

File diff suppressed because it is too large Load diff

View file

@ -146,7 +146,7 @@ func (msg *Message) GetSignalStrength() float64 {
if msg.Signal == 0 {
return -50.0 // Minimum detectable signal
}
return float64(msg.Signal)*(-50.0/255.0)
return float64(msg.Signal) * (-50.0 / 255.0)
}
// GetICAO24 extracts the ICAO 24-bit address from Mode S messages

View file

@ -222,4 +222,3 @@ func getRegistrationFromICAO(icao string) string {
return icao
}
}

View file

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#00a8ff" stroke="#ffffff" stroke-width="1">
<path d="M12 2l-2 16 2-2 2 2-2-16z"/>
<path d="M4 10l8-2-1 2-7 0z"/>
<path d="M20 10l-8-2 1 2 7 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 224 B

View file

@ -1,488 +0,0 @@
* {
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;
}
}

View file

@ -1 +0,0 @@
data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

View file

@ -1,226 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SkyView - Multi-Source ADS-B Aircraft Tracker</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
<!-- Three.js for 3D radar (ES modules) -->
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.158.0/examples/jsm/"
}
}
</script>
<!-- Custom CSS -->
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div id="app">
<header class="header">
<h1>SkyView</h1>
<!-- Status indicators -->
<div class="status-section">
<div class="clock-display">
<div class="clock" id="utc-clock">
<div class="clock-face">
<div class="clock-hand hour-hand" id="utc-hour"></div>
<div class="clock-hand minute-hand" id="utc-minute"></div>
</div>
<div class="clock-label">UTC</div>
</div>
<div class="clock" id="update-clock">
<div class="clock-face">
<div class="clock-hand hour-hand" id="update-hour"></div>
<div class="clock-hand minute-hand" id="update-minute"></div>
</div>
<div class="clock-label">Last Update</div>
</div>
</div>
</div>
<!-- Summary stats -->
<div class="stats-summary">
<span id="aircraft-count">0 aircraft</span>
<span id="sources-count">0 sources</span>
<span id="connection-status" class="connection-status disconnected">Connecting...</span>
</div>
</header>
<main class="main-content">
<!-- View selection tabs -->
<div class="view-toggle">
<button id="map-view-btn" class="view-btn active">Map</button>
<button id="table-view-btn" class="view-btn">Table</button>
<button id="stats-view-btn" class="view-btn">Statistics</button>
<button id="coverage-view-btn" class="view-btn">Coverage</button>
<button id="radar3d-view-btn" class="view-btn">3D Radar</button>
</div>
<!-- Map View -->
<div id="map-view" class="view active">
<div id="map"></div>
<!-- Map controls -->
<div class="map-controls">
<button id="center-map" title="Center on aircraft">Center Map</button>
<button id="toggle-trails" title="Show/hide aircraft trails">Show Trails</button>
<button id="toggle-range" title="Show/hide range circles">Show Range</button>
<button id="toggle-sources" title="Show/hide source locations">Show Sources</button>
</div>
<!-- Legend -->
<div class="legend">
<h4>Aircraft Types</h4>
<div class="legend-item">
<span class="legend-icon commercial"></span>
<span>Commercial</span>
</div>
<div class="legend-item">
<span class="legend-icon cargo"></span>
<span>Cargo</span>
</div>
<div class="legend-item">
<span class="legend-icon military"></span>
<span>Military</span>
</div>
<div class="legend-item">
<span class="legend-icon ga"></span>
<span>General Aviation</span>
</div>
<div class="legend-item">
<span class="legend-icon ground"></span>
<span>Ground</span>
</div>
<h4>Sources</h4>
<div id="sources-legend"></div>
</div>
</div>
<!-- Table View -->
<div id="table-view" class="view">
<div class="table-controls">
<input type="text" id="search-input" placeholder="Search by flight, ICAO, or squawk...">
<select id="sort-select">
<option value="distance">Distance</option>
<option value="altitude">Altitude</option>
<option value="speed">Speed</option>
<option value="flight">Flight</option>
<option value="icao">ICAO</option>
<option value="squawk">Squawk</option>
<option value="signal">Signal</option>
<option value="age">Age</option>
</select>
<select id="source-filter">
<option value="">All Sources</option>
</select>
</div>
<div class="table-container">
<table id="aircraft-table">
<thead>
<tr>
<th>ICAO</th>
<th>Flight</th>
<th>Squawk</th>
<th>Altitude</th>
<th>Speed</th>
<th>Distance</th>
<th>Track</th>
<th>Sources</th>
<th>Signal</th>
<th>Age</th>
</tr>
</thead>
<tbody id="aircraft-tbody">
</tbody>
</table>
</div>
</div>
<!-- Statistics View -->
<div id="stats-view" class="view">
<div class="stats-grid">
<div class="stat-card">
<h3>Total Aircraft</h3>
<div class="stat-value" id="total-aircraft">0</div>
</div>
<div class="stat-card">
<h3>Active Sources</h3>
<div class="stat-value" id="active-sources">0</div>
</div>
<div class="stat-card">
<h3>Messages/sec</h3>
<div class="stat-value" id="messages-sec">0</div>
</div>
<div class="stat-card">
<h3>Max Range</h3>
<div class="stat-value" id="max-range">0 km</div>
</div>
</div>
<!-- Charts -->
<div class="charts-container">
<div class="chart-card">
<h3>Aircraft Count Timeline</h3>
<canvas id="aircraft-chart"></canvas>
</div>
<div class="chart-card">
<h3>Message Rate by Source</h3>
<canvas id="message-chart"></canvas>
</div>
<div class="chart-card">
<h3>Signal Strength Distribution</h3>
<canvas id="signal-chart"></canvas>
</div>
<div class="chart-card">
<h3>Altitude Distribution</h3>
<canvas id="altitude-chart"></canvas>
</div>
</div>
</div>
<!-- Coverage View -->
<div id="coverage-view" class="view">
<div class="coverage-controls">
<select id="coverage-source">
<option value="">Select Source</option>
</select>
<button id="toggle-heatmap">Toggle Heatmap</button>
</div>
<div id="coverage-map"></div>
</div>
<!-- 3D Radar View -->
<div id="radar3d-view" class="view">
<div class="radar3d-controls">
<button id="radar3d-reset">Reset View</button>
<button id="radar3d-auto-rotate">Auto Rotate</button>
<label>
<input type="range" id="radar3d-range" min="10" max="500" value="100">
Range: <span id="radar3d-range-value">100</span> km
</label>
</div>
<div id="radar3d-container"></div>
</div>
</main>
</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Custom JS -->
<script type="module" src="/static/js/app.js"></script>
</body>
</html>