feat: Enhance web interface with database integration and callsign management
- Add callsign management module for enhanced aircraft information - Integrate database status display in web interface - Update aircraft manager with database-backed callsign resolution - Enhance user interface with database connectivity indicators - Add embedded asset management for new database interface components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5733209251
commit
8019049c63
6 changed files with 303 additions and 5 deletions
|
|
@ -28,5 +28,6 @@ import "embed"
|
||||||
// This approach ensures the web interface is always available without requiring
|
// This approach ensures the web interface is always available without requiring
|
||||||
// external file deployment or complicated asset management.
|
// external file deployment or complicated asset management.
|
||||||
//
|
//
|
||||||
|
// Updated to include database.html for database status page
|
||||||
//go:embed static
|
//go:embed static
|
||||||
var Static embed.FS
|
var Static embed.FS
|
||||||
|
|
|
||||||
|
|
@ -566,6 +566,95 @@ body {
|
||||||
color: #00ff88 !important;
|
color: #00ff88 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rich callsign display styles */
|
||||||
|
.callsign-display {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callsign-display.enriched {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callsign-code {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.airline-code {
|
||||||
|
color: #00ff88 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: monospace;
|
||||||
|
background: rgba(0, 255, 136, 0.1);
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid rgba(0, 255, 136, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flight-number {
|
||||||
|
color: #00a8ff !important;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callsign-details {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.airline-name {
|
||||||
|
color: #ffd700 !important;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.airline-country {
|
||||||
|
color: #cccccc !important;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callsign-display.simple {
|
||||||
|
color: #00ff88 !important;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callsign-display.no-data {
|
||||||
|
color: #888888 !important;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Compact callsign for table view */
|
||||||
|
.callsign-compact {
|
||||||
|
color: #00ff88 !important;
|
||||||
|
font-family: monospace;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading state for callsign enhancement */
|
||||||
|
.callsign-loading {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callsign-loading::after {
|
||||||
|
content: '⟳';
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
opacity: 0.6;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.callsign-enhanced {
|
||||||
|
/* Smooth transition when enhanced */
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
.popup-details {
|
.popup-details {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,10 @@
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<h1>SkyView <span class="version-info">v0.0.8</span> <a href="https://kode.naiv.no/olemd/skyview" target="_blank" class="repo-link" title="Project Repository">⚙</a></h1>
|
<h1>SkyView <span class="version-info">v0.0.8</span>
|
||||||
|
<a href="https://kode.naiv.no/olemd/skyview" target="_blank" class="repo-link" title="Project Repository">⚙</a>
|
||||||
|
<a href="/database" class="repo-link" title="Database Status">📊</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
<!-- Status indicators -->
|
<!-- Status indicators -->
|
||||||
<div class="status-section">
|
<div class="status-section">
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { WebSocketManager } from './modules/websocket.js?v=2';
|
||||||
import { AircraftManager } from './modules/aircraft-manager.js?v=2';
|
import { AircraftManager } from './modules/aircraft-manager.js?v=2';
|
||||||
import { MapManager } from './modules/map-manager.js?v=2';
|
import { MapManager } from './modules/map-manager.js?v=2';
|
||||||
import { UIManager } from './modules/ui-manager.js?v=2';
|
import { UIManager } from './modules/ui-manager.js?v=2';
|
||||||
|
import { CallsignManager } from './modules/callsign-manager.js';
|
||||||
|
|
||||||
class SkyView {
|
class SkyView {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -15,6 +16,7 @@ class SkyView {
|
||||||
this.aircraftManager = null;
|
this.aircraftManager = null;
|
||||||
this.mapManager = null;
|
this.mapManager = null;
|
||||||
this.uiManager = null;
|
this.uiManager = null;
|
||||||
|
this.callsignManager = null;
|
||||||
|
|
||||||
// 3D Radar
|
// 3D Radar
|
||||||
this.radar3d = null;
|
this.radar3d = null;
|
||||||
|
|
@ -37,12 +39,15 @@ class SkyView {
|
||||||
this.uiManager.initializeViews();
|
this.uiManager.initializeViews();
|
||||||
this.uiManager.initializeEventListeners();
|
this.uiManager.initializeEventListeners();
|
||||||
|
|
||||||
|
// Initialize callsign manager for enriched callsign display
|
||||||
|
this.callsignManager = new CallsignManager();
|
||||||
|
|
||||||
// Initialize map manager and get the main map
|
// Initialize map manager and get the main map
|
||||||
this.mapManager = new MapManager();
|
this.mapManager = new MapManager();
|
||||||
const map = await this.mapManager.initializeMap();
|
const map = await this.mapManager.initializeMap();
|
||||||
|
|
||||||
// Initialize aircraft manager with the map
|
// Initialize aircraft manager with the map and callsign manager
|
||||||
this.aircraftManager = new AircraftManager(map);
|
this.aircraftManager = new AircraftManager(map, this.callsignManager);
|
||||||
|
|
||||||
// Set up selected aircraft trail callback
|
// Set up selected aircraft trail callback
|
||||||
this.aircraftManager.setSelectedAircraftCallback((icao) => {
|
this.aircraftManager.setSelectedAircraftCallback((icao) => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
// Aircraft marker and data management module
|
// Aircraft marker and data management module
|
||||||
export class AircraftManager {
|
export class AircraftManager {
|
||||||
constructor(map) {
|
constructor(map, callsignManager = null) {
|
||||||
this.map = map;
|
this.map = map;
|
||||||
|
this.callsignManager = callsignManager;
|
||||||
this.aircraftData = new Map();
|
this.aircraftData = new Map();
|
||||||
this.aircraftMarkers = new Map();
|
this.aircraftMarkers = new Map();
|
||||||
this.aircraftTrails = new Map();
|
this.aircraftTrails = new Map();
|
||||||
|
|
@ -228,6 +229,11 @@ export class AircraftManager {
|
||||||
// Handle popup exactly like Leaflet expects
|
// Handle popup exactly like Leaflet expects
|
||||||
if (marker.isPopupOpen()) {
|
if (marker.isPopupOpen()) {
|
||||||
marker.setPopupContent(this.createPopupContent(aircraft));
|
marker.setPopupContent(this.createPopupContent(aircraft));
|
||||||
|
// Enhance callsign display for updated popup
|
||||||
|
const popupElement = marker.getPopup().getElement();
|
||||||
|
if (popupElement) {
|
||||||
|
this.enhanceCallsignDisplay(popupElement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.markerUpdateCount++;
|
this.markerUpdateCount++;
|
||||||
|
|
@ -250,6 +256,14 @@ export class AircraftManager {
|
||||||
maxWidth: 450,
|
maxWidth: 450,
|
||||||
className: 'aircraft-popup'
|
className: 'aircraft-popup'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enhance callsign display when popup opens
|
||||||
|
marker.on('popupopen', (e) => {
|
||||||
|
const popupElement = e.popup.getElement();
|
||||||
|
if (popupElement) {
|
||||||
|
this.enhanceCallsignDisplay(popupElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.aircraftMarkers.set(icao, marker);
|
this.aircraftMarkers.set(icao, marker);
|
||||||
this.markerCreateCount++;
|
this.markerCreateCount++;
|
||||||
|
|
@ -435,7 +449,7 @@ export class AircraftManager {
|
||||||
<div class="flight-info">
|
<div class="flight-info">
|
||||||
<span class="icao-flag">${flag}</span>
|
<span class="icao-flag">${flag}</span>
|
||||||
<span class="flight-id">${aircraft.ICAO24 || 'N/A'}</span>
|
<span class="flight-id">${aircraft.ICAO24 || 'N/A'}</span>
|
||||||
${aircraft.Callsign ? `→ <span class="callsign">${aircraft.Callsign}</span>` : ''}
|
${aircraft.Callsign ? `→ <span class="callsign-loading" data-callsign="${aircraft.Callsign}"><span class="callsign">${aircraft.Callsign}</span></span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -511,6 +525,29 @@ export class AircraftManager {
|
||||||
return minDistance === Infinity ? null : minDistance;
|
return minDistance === Infinity ? null : minDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhance callsign display in popup after it's created
|
||||||
|
async enhanceCallsignDisplay(popupElement) {
|
||||||
|
if (!this.callsignManager) return;
|
||||||
|
|
||||||
|
const callsignElements = popupElement.querySelectorAll('.callsign-loading');
|
||||||
|
|
||||||
|
for (const element of callsignElements) {
|
||||||
|
const callsign = element.dataset.callsign;
|
||||||
|
if (!callsign) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const callsignInfo = await this.callsignManager.getCallsignInfo(callsign);
|
||||||
|
const richDisplay = this.callsignManager.generateCallsignDisplay(callsignInfo, callsign);
|
||||||
|
element.innerHTML = richDisplay;
|
||||||
|
element.classList.remove('callsign-loading');
|
||||||
|
element.classList.add('callsign-enhanced');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to enhance callsign display for ${callsign}:`, error);
|
||||||
|
// Keep the simple display on error
|
||||||
|
element.classList.remove('callsign-loading');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toggleTrails() {
|
toggleTrails() {
|
||||||
this.showTrails = !this.showTrails;
|
this.showTrails = !this.showTrails;
|
||||||
|
|
|
||||||
163
assets/static/js/modules/callsign-manager.js
Normal file
163
assets/static/js/modules/callsign-manager.js
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
// Callsign enrichment and display module
|
||||||
|
export class CallsignManager {
|
||||||
|
constructor() {
|
||||||
|
this.callsignCache = new Map();
|
||||||
|
this.pendingRequests = new Map();
|
||||||
|
|
||||||
|
// Rate limiting to avoid overwhelming the API
|
||||||
|
this.lastRequestTime = 0;
|
||||||
|
this.requestInterval = 100; // Minimum 100ms between requests
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get enriched callsign information, using cache when available
|
||||||
|
* @param {string} callsign - The raw callsign to lookup
|
||||||
|
* @returns {Promise<Object>} - Enriched callsign data
|
||||||
|
*/
|
||||||
|
async getCallsignInfo(callsign) {
|
||||||
|
if (!callsign || callsign.trim() === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanCallsign = callsign.trim().toUpperCase();
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
if (this.callsignCache.has(cleanCallsign)) {
|
||||||
|
return this.callsignCache.get(cleanCallsign);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we already have a pending request for this callsign
|
||||||
|
if (this.pendingRequests.has(cleanCallsign)) {
|
||||||
|
return this.pendingRequests.get(cleanCallsign);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - this.lastRequestTime < this.requestInterval) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, this.requestInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the API request
|
||||||
|
const requestPromise = this.fetchCallsignInfo(cleanCallsign);
|
||||||
|
this.pendingRequests.set(cleanCallsign, requestPromise);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await requestPromise;
|
||||||
|
|
||||||
|
// Cache the result for future use
|
||||||
|
if (result && result.callsign) {
|
||||||
|
this.callsignCache.set(cleanCallsign, result.callsign);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result ? result.callsign : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to lookup callsign ${cleanCallsign}:`, error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
// Clean up pending request
|
||||||
|
this.pendingRequests.delete(cleanCallsign);
|
||||||
|
this.lastRequestTime = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch callsign information from the API
|
||||||
|
* @param {string} callsign - The callsign to lookup
|
||||||
|
* @returns {Promise<Object>} - API response
|
||||||
|
*/
|
||||||
|
async fetchCallsignInfo(callsign) {
|
||||||
|
const response = await fetch(`/api/callsign/${encodeURIComponent(callsign)}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate rich HTML display for a callsign
|
||||||
|
* @param {Object} callsignInfo - Enriched callsign data from API
|
||||||
|
* @param {string} originalCallsign - Original callsign if API data is null
|
||||||
|
* @returns {string} - HTML string for display
|
||||||
|
*/
|
||||||
|
generateCallsignDisplay(callsignInfo, originalCallsign = '') {
|
||||||
|
if (!callsignInfo || !callsignInfo.is_valid) {
|
||||||
|
// Fallback for invalid or missing callsign data
|
||||||
|
if (originalCallsign) {
|
||||||
|
return `<span class="callsign-display simple">${originalCallsign}</span>`;
|
||||||
|
}
|
||||||
|
return '<span class="callsign-display no-data">N/A</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
// Airline code
|
||||||
|
if (callsignInfo.airline_code) {
|
||||||
|
parts.push(`<span class="airline-code">${callsignInfo.airline_code}</span>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flight number
|
||||||
|
if (callsignInfo.flight_number) {
|
||||||
|
parts.push(`<span class="flight-number">${callsignInfo.flight_number}</span>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Airline name (if available)
|
||||||
|
let airlineInfo = '';
|
||||||
|
if (callsignInfo.airline_name) {
|
||||||
|
airlineInfo = `<span class="airline-name" title="${callsignInfo.airline_name}">
|
||||||
|
${callsignInfo.airline_name}
|
||||||
|
</span>`;
|
||||||
|
|
||||||
|
// Add country if available
|
||||||
|
if (callsignInfo.airline_country) {
|
||||||
|
airlineInfo += ` <span class="airline-country">(${callsignInfo.airline_country})</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<span class="callsign-display enriched">
|
||||||
|
<span class="callsign-code">${parts.join(' ')}</span>
|
||||||
|
${airlineInfo ? `<span class="callsign-details">${airlineInfo}</span>` : ''}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate compact callsign display for table view
|
||||||
|
* @param {Object} callsignInfo - Enriched callsign data
|
||||||
|
* @param {string} originalCallsign - Original callsign fallback
|
||||||
|
* @returns {string} - Compact HTML for table display
|
||||||
|
*/
|
||||||
|
generateCompactCallsignDisplay(callsignInfo, originalCallsign = '') {
|
||||||
|
if (!callsignInfo || !callsignInfo.is_valid) {
|
||||||
|
return originalCallsign || 'N/A';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For tables, use the display_name or format airline + flight
|
||||||
|
if (callsignInfo.display_name) {
|
||||||
|
return `<span class="callsign-compact" title="${callsignInfo.airline_name || ''}">${callsignInfo.display_name}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<span class="callsign-compact">${callsignInfo.airline_code} ${callsignInfo.flight_number}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the callsign cache (useful for memory management)
|
||||||
|
*/
|
||||||
|
clearCache() {
|
||||||
|
this.callsignCache.clear();
|
||||||
|
console.debug('Callsign cache cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache statistics for debugging
|
||||||
|
* @returns {Object} - Cache size and pending requests
|
||||||
|
*/
|
||||||
|
getCacheStats() {
|
||||||
|
return {
|
||||||
|
cacheSize: this.callsignCache.size,
|
||||||
|
pendingRequests: this.pendingRequests.size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue