mirror of
https://github.com/smittix/intercept.git
synced 2026-04-25 07:10:00 -07:00
Replace IBM Plex Mono, Space Mono, and JetBrains Mono with Roboto Condensed across all CSS variables, inline styles, canvas ctx.font references, and Google Fonts CDN links. Updates 28 files covering templates, stylesheets, and JS modules for consistent typography. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
986 lines
35 KiB
JavaScript
986 lines
35 KiB
JavaScript
/**
|
|
* Settings Manager - Handles offline mode and application settings
|
|
*/
|
|
|
|
const Settings = {
|
|
// Default settings
|
|
defaults: {
|
|
'offline.enabled': false,
|
|
'offline.assets_source': 'cdn',
|
|
'offline.fonts_source': 'cdn',
|
|
'offline.tile_provider': 'cartodb_dark_cyan',
|
|
'offline.tile_server_url': ''
|
|
},
|
|
|
|
// Tile provider configurations
|
|
tileProviders: {
|
|
openstreetmap: {
|
|
url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
subdomains: 'abc'
|
|
},
|
|
cartodb_dark: {
|
|
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png',
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
|
subdomains: 'abcd'
|
|
},
|
|
cartodb_dark_cyan: {
|
|
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png',
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
|
subdomains: 'abcd',
|
|
options: {
|
|
className: 'tile-layer-cyan'
|
|
}
|
|
},
|
|
cartodb_light: {
|
|
url: 'https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png',
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> © <a href="https://carto.com/">CARTO</a>',
|
|
subdomains: 'abcd'
|
|
},
|
|
esri_world: {
|
|
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
|
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
|
|
subdomains: null
|
|
}
|
|
},
|
|
|
|
// Registry of maps that can be updated
|
|
_registeredMaps: [],
|
|
|
|
// Current settings cache
|
|
_cache: {},
|
|
|
|
/**
|
|
* Initialize settings - load from server/localStorage
|
|
*/
|
|
async init() {
|
|
try {
|
|
const response = await fetch('/offline/settings');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
this._cache = { ...this.defaults, ...data.settings };
|
|
} else {
|
|
// Fall back to localStorage
|
|
this._loadFromLocalStorage();
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to load settings from server, using localStorage:', e);
|
|
this._loadFromLocalStorage();
|
|
}
|
|
|
|
this._updateUI();
|
|
return this._cache;
|
|
},
|
|
|
|
/**
|
|
* Load settings from localStorage
|
|
*/
|
|
_loadFromLocalStorage() {
|
|
const stored = localStorage.getItem('intercept_settings');
|
|
if (stored) {
|
|
try {
|
|
this._cache = { ...this.defaults, ...JSON.parse(stored) };
|
|
} catch (e) {
|
|
this._cache = { ...this.defaults };
|
|
}
|
|
} else {
|
|
this._cache = { ...this.defaults };
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Save a setting to server and localStorage
|
|
*/
|
|
async _save(key, value) {
|
|
this._cache[key] = value;
|
|
|
|
// Save to localStorage as backup
|
|
localStorage.setItem('intercept_settings', JSON.stringify(this._cache));
|
|
|
|
// Save to server
|
|
try {
|
|
await fetch('/offline/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ key, value })
|
|
});
|
|
} catch (e) {
|
|
console.warn('Failed to save setting to server:', e);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get a setting value
|
|
*/
|
|
get(key) {
|
|
return this._cache[key] ?? this.defaults[key];
|
|
},
|
|
|
|
/**
|
|
* Toggle offline mode master switch
|
|
*/
|
|
async toggleOfflineMode(enabled) {
|
|
await this._save('offline.enabled', enabled);
|
|
|
|
if (enabled) {
|
|
// When enabling offline mode, also switch assets and fonts to local
|
|
await this._save('offline.assets_source', 'local');
|
|
await this._save('offline.fonts_source', 'local');
|
|
}
|
|
|
|
this._updateUI();
|
|
this._showReloadPrompt();
|
|
},
|
|
|
|
/**
|
|
* Set asset source (cdn or local)
|
|
*/
|
|
async setAssetSource(source) {
|
|
await this._save('offline.assets_source', source);
|
|
this._showReloadPrompt();
|
|
},
|
|
|
|
/**
|
|
* Set fonts source (cdn or local)
|
|
*/
|
|
async setFontsSource(source) {
|
|
await this._save('offline.fonts_source', source);
|
|
this._showReloadPrompt();
|
|
},
|
|
|
|
/**
|
|
* Set tile provider
|
|
*/
|
|
async setTileProvider(provider) {
|
|
await this._save('offline.tile_provider', provider);
|
|
|
|
// Show/hide custom URL input
|
|
const customRow = document.getElementById('customTileUrlRow');
|
|
if (customRow) {
|
|
customRow.style.display = provider === 'custom' ? 'block' : 'none';
|
|
}
|
|
|
|
// If not custom and we have a map, update tiles immediately
|
|
if (provider !== 'custom') {
|
|
this._updateMapTiles();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set custom tile server URL
|
|
*/
|
|
async setCustomTileUrl(url) {
|
|
await this._save('offline.tile_server_url', url);
|
|
this._updateMapTiles();
|
|
},
|
|
|
|
/**
|
|
* Get current tile configuration
|
|
*/
|
|
getTileConfig() {
|
|
const provider = this.get('offline.tile_provider');
|
|
|
|
if (provider === 'custom') {
|
|
const customUrl = this.get('offline.tile_server_url');
|
|
return {
|
|
url: customUrl || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
|
attribution: 'Custom Tile Server',
|
|
subdomains: 'abc'
|
|
};
|
|
}
|
|
|
|
return this.tileProviders[provider] || this.tileProviders.cartodb_dark;
|
|
},
|
|
|
|
/**
|
|
* Register a map to receive tile updates when settings change
|
|
* @param {L.Map} map - Leaflet map instance
|
|
*/
|
|
registerMap(map) {
|
|
if (map && typeof map.eachLayer === 'function' && !this._registeredMaps.includes(map)) {
|
|
this._registeredMaps.push(map);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Unregister a map
|
|
* @param {L.Map} map - Leaflet map instance
|
|
*/
|
|
unregisterMap(map) {
|
|
const idx = this._registeredMaps.indexOf(map);
|
|
if (idx > -1) {
|
|
this._registeredMaps.splice(idx, 1);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Create a tile layer using current settings
|
|
* @returns {L.TileLayer} Configured tile layer
|
|
*/
|
|
createTileLayer() {
|
|
const config = this.getTileConfig();
|
|
const options = {
|
|
attribution: config.attribution,
|
|
maxZoom: 19,
|
|
...(config.options || {})
|
|
};
|
|
if (config.subdomains) {
|
|
options.subdomains = config.subdomains;
|
|
}
|
|
return L.tileLayer(config.url, options);
|
|
},
|
|
|
|
/**
|
|
* Check if local assets are available
|
|
*/
|
|
async checkAssets() {
|
|
const assets = {
|
|
leaflet: [
|
|
'/static/vendor/leaflet/leaflet.js',
|
|
'/static/vendor/leaflet/leaflet.css'
|
|
],
|
|
chartjs: [
|
|
'/static/vendor/chartjs/chart.umd.min.js'
|
|
],
|
|
inter: [
|
|
'/static/vendor/fonts/Inter-Regular.woff2'
|
|
],
|
|
jetbrains: [
|
|
'/static/vendor/fonts/JetBrainsMono-Regular.woff2'
|
|
]
|
|
};
|
|
|
|
const results = {};
|
|
|
|
for (const [name, urls] of Object.entries(assets)) {
|
|
const statusEl = document.getElementById(`status${name.charAt(0).toUpperCase() + name.slice(1)}`);
|
|
if (statusEl) {
|
|
statusEl.textContent = 'Checking...';
|
|
statusEl.className = 'asset-badge checking';
|
|
}
|
|
|
|
let available = true;
|
|
for (const url of urls) {
|
|
try {
|
|
const response = await fetch(url, { method: 'HEAD' });
|
|
if (!response.ok) {
|
|
available = false;
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
available = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
results[name] = available;
|
|
|
|
if (statusEl) {
|
|
statusEl.textContent = available ? 'Available' : 'Missing';
|
|
statusEl.className = `asset-badge ${available ? 'available' : 'missing'}`;
|
|
}
|
|
}
|
|
|
|
return results;
|
|
},
|
|
|
|
/**
|
|
* Update UI elements to reflect current settings
|
|
*/
|
|
_updateUI() {
|
|
// Offline mode toggle
|
|
const offlineEnabled = document.getElementById('offlineEnabled');
|
|
if (offlineEnabled) {
|
|
offlineEnabled.checked = this.get('offline.enabled');
|
|
}
|
|
|
|
// Assets source
|
|
const assetsSource = document.getElementById('assetsSource');
|
|
if (assetsSource) {
|
|
assetsSource.value = this.get('offline.assets_source');
|
|
}
|
|
|
|
// Fonts source
|
|
const fontsSource = document.getElementById('fontsSource');
|
|
if (fontsSource) {
|
|
fontsSource.value = this.get('offline.fonts_source');
|
|
}
|
|
|
|
// Tile provider
|
|
const tileProvider = document.getElementById('tileProvider');
|
|
if (tileProvider) {
|
|
tileProvider.value = this.get('offline.tile_provider');
|
|
}
|
|
|
|
// Custom tile URL
|
|
const customTileUrl = document.getElementById('customTileUrl');
|
|
if (customTileUrl) {
|
|
customTileUrl.value = this.get('offline.tile_server_url') || '';
|
|
}
|
|
|
|
// Show/hide custom URL row
|
|
const customRow = document.getElementById('customTileUrlRow');
|
|
if (customRow) {
|
|
customRow.style.display = this.get('offline.tile_provider') === 'custom' ? 'block' : 'none';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update map tiles on all known maps
|
|
*/
|
|
_updateMapTiles() {
|
|
// Combine registered maps with common window map variables
|
|
const windowMaps = [
|
|
window.map,
|
|
window.leafletMap,
|
|
window.aprsMap,
|
|
window.radarMap,
|
|
window.vesselMap,
|
|
window.groundMap,
|
|
window.groundTrackMap,
|
|
window.meshMap,
|
|
window.issMap
|
|
].filter(m => m && typeof m.eachLayer === 'function');
|
|
|
|
// Combine with registered maps, removing duplicates
|
|
const allMaps = [...new Set([...this._registeredMaps, ...windowMaps])];
|
|
|
|
if (allMaps.length === 0) return;
|
|
|
|
const config = this.getTileConfig();
|
|
|
|
allMaps.forEach(map => {
|
|
// Remove existing tile layers
|
|
map.eachLayer(layer => {
|
|
if (layer instanceof L.TileLayer) {
|
|
map.removeLayer(layer);
|
|
}
|
|
});
|
|
|
|
// Add new tile layer
|
|
const options = {
|
|
attribution: config.attribution,
|
|
maxZoom: 19,
|
|
...(config.options || {})
|
|
};
|
|
if (config.subdomains) {
|
|
options.subdomains = config.subdomains;
|
|
}
|
|
|
|
L.tileLayer(config.url, options).addTo(map);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Show reload prompt
|
|
*/
|
|
_showReloadPrompt() {
|
|
// Create or update reload prompt
|
|
let prompt = document.getElementById('settingsReloadPrompt');
|
|
if (!prompt) {
|
|
prompt = document.createElement('div');
|
|
prompt.id = 'settingsReloadPrompt';
|
|
prompt.style.cssText = `
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
background: var(--bg-dark, #0a0a0f);
|
|
border: 1px solid var(--accent-cyan, #00d4ff);
|
|
border-radius: 8px;
|
|
padding: 12px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
z-index: 10001;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
`;
|
|
prompt.innerHTML = `
|
|
<span style="color: var(--text-primary, #e0e0e0); font-size: 13px;">
|
|
Reload to apply changes
|
|
</span>
|
|
<button onclick="location.reload()" style="
|
|
background: var(--accent-cyan, #00d4ff);
|
|
border: none;
|
|
color: #000;
|
|
padding: 6px 12px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
">Reload</button>
|
|
<button onclick="this.parentElement.remove()" style="
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-muted, #666);
|
|
font-size: 18px;
|
|
cursor: pointer;
|
|
padding: 0 4px;
|
|
">×</button>
|
|
`;
|
|
document.body.appendChild(prompt);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Settings modal functions
|
|
function showSettings() {
|
|
const modal = document.getElementById('settingsModal');
|
|
if (modal) {
|
|
modal.classList.add('active');
|
|
Settings.init().then(() => {
|
|
Settings.checkAssets();
|
|
});
|
|
}
|
|
}
|
|
|
|
function hideSettings() {
|
|
const modal = document.getElementById('settingsModal');
|
|
if (modal) {
|
|
modal.classList.remove('active');
|
|
}
|
|
}
|
|
|
|
function switchSettingsTab(tabName) {
|
|
// Update tab buttons
|
|
document.querySelectorAll('.settings-tab').forEach(tab => {
|
|
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
|
});
|
|
|
|
// Update sections
|
|
document.querySelectorAll('.settings-section').forEach(section => {
|
|
section.classList.toggle('active', section.id === `settings-${tabName}`);
|
|
});
|
|
|
|
// Load tools/dependencies when that tab is selected
|
|
if (tabName === 'tools') {
|
|
loadSettingsTools();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load tool dependencies into settings modal
|
|
*/
|
|
function loadSettingsTools() {
|
|
const content = document.getElementById('settingsToolsContent');
|
|
if (!content) return;
|
|
|
|
content.innerHTML = '<div style="text-align: center; padding: 30px; color: var(--text-dim);">Loading dependencies...</div>';
|
|
|
|
fetch('/dependencies')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.status !== 'success') {
|
|
content.innerHTML = '<div style="color: var(--accent-red);">Error loading dependencies</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
let totalMissing = 0;
|
|
|
|
for (const [modeKey, mode] of Object.entries(data.modes)) {
|
|
const statusColor = mode.ready ? 'var(--accent-green)' : 'var(--accent-red)';
|
|
const statusIcon = mode.ready ? '✓' : '✗';
|
|
|
|
html += `
|
|
<div style="background: var(--bg-tertiary); border-radius: 6px; padding: 12px; margin-bottom: 10px; border-left: 3px solid ${statusColor};">
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
|
<span style="font-weight: 600; color: var(--accent-cyan); font-size: 13px;">${mode.name}</span>
|
|
<span style="color: ${statusColor}; font-size: 11px; font-weight: bold;">${statusIcon} ${mode.ready ? 'Ready' : 'Missing'}</span>
|
|
</div>
|
|
<div style="display: grid; gap: 6px;">
|
|
`;
|
|
|
|
for (const [toolName, tool] of Object.entries(mode.tools)) {
|
|
const installed = tool.installed;
|
|
const dotColor = installed ? 'var(--accent-green)' : 'var(--accent-red)';
|
|
const requiredBadge = tool.required ? '<span style="background: var(--accent-orange); color: #000; padding: 1px 4px; border-radius: 3px; font-size: 9px; margin-left: 4px;">REQ</span>' : '';
|
|
|
|
if (!installed) totalMissing++;
|
|
|
|
let installCmd = '';
|
|
if (tool.install) {
|
|
if (tool.install.pip) {
|
|
installCmd = tool.install.pip;
|
|
} else if (data.pkg_manager && tool.install[data.pkg_manager]) {
|
|
installCmd = tool.install[data.pkg_manager];
|
|
} else if (tool.install.manual) {
|
|
installCmd = tool.install.manual;
|
|
}
|
|
}
|
|
|
|
html += `
|
|
<div style="display: flex; align-items: center; gap: 8px; padding: 6px 8px; background: var(--bg-secondary); border-radius: 4px; font-size: 11px;">
|
|
<span style="color: ${dotColor}; font-size: 12px;">●</span>
|
|
<div style="flex: 1; min-width: 0;">
|
|
<span style="font-weight: 500;">${toolName}${requiredBadge}</span>
|
|
<div style="font-size: 10px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${tool.description}</div>
|
|
</div>
|
|
${!installed && installCmd ? `
|
|
<code style="font-size: 9px; background: var(--bg-tertiary); padding: 2px 6px; border-radius: 3px; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${installCmd}">${installCmd}</code>
|
|
` : ''}
|
|
<span style="font-size: 10px; color: ${dotColor}; font-weight: bold; min-width: 45px; text-align: right;">${installed ? 'OK' : 'MISSING'}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
html += '</div></div>';
|
|
}
|
|
|
|
// Summary at top
|
|
const summaryHtml = `
|
|
<div style="background: ${totalMissing > 0 ? 'rgba(255, 100, 0, 0.1)' : 'rgba(0, 255, 100, 0.1)'}; border: 1px solid ${totalMissing > 0 ? 'var(--accent-orange)' : 'var(--accent-green)'}; border-radius: 6px; padding: 10px 12px; margin-bottom: 12px;">
|
|
<div style="font-size: 13px; font-weight: bold; color: ${totalMissing > 0 ? 'var(--accent-orange)' : 'var(--accent-green)'};">
|
|
${totalMissing > 0 ? '⚠️ ' + totalMissing + ' tool(s) not found' : '✓ All tools installed'}
|
|
</div>
|
|
<div style="font-size: 11px; color: var(--text-dim); margin-top: 3px;">
|
|
OS: ${data.os} | Package Manager: ${data.pkg_manager}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
content.innerHTML = summaryHtml + html;
|
|
})
|
|
.catch(err => {
|
|
content.innerHTML = '<div style="color: var(--accent-red);">Error loading dependencies: ' + err.message + '</div>';
|
|
});
|
|
}
|
|
|
|
// Initialize settings on page load
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
Settings.init();
|
|
});
|
|
|
|
// =============================================================================
|
|
// Location Settings Functions
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Load and display current observer location
|
|
*/
|
|
function loadObserverLocation() {
|
|
let lat = localStorage.getItem('observerLat');
|
|
let lon = localStorage.getItem('observerLon');
|
|
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
|
const shared = ObserverLocation.getShared();
|
|
lat = shared.lat.toString();
|
|
lon = shared.lon.toString();
|
|
}
|
|
|
|
const latInput = document.getElementById('observerLatInput');
|
|
const lonInput = document.getElementById('observerLonInput');
|
|
const currentLatDisplay = document.getElementById('currentLatDisplay');
|
|
const currentLonDisplay = document.getElementById('currentLonDisplay');
|
|
|
|
if (latInput && lat) latInput.value = lat;
|
|
if (lonInput && lon) lonInput.value = lon;
|
|
|
|
if (currentLatDisplay) {
|
|
currentLatDisplay.textContent = lat ? parseFloat(lat).toFixed(4) + '°' : 'Not set';
|
|
}
|
|
if (currentLonDisplay) {
|
|
currentLonDisplay.textContent = lon ? parseFloat(lon).toFixed(4) + '°' : 'Not set';
|
|
}
|
|
|
|
// Sync dashboard-specific location keys for backward compatibility
|
|
if (lat && lon) {
|
|
const locationObj = JSON.stringify({ lat: parseFloat(lat), lon: parseFloat(lon) });
|
|
if (!localStorage.getItem('observerLocation')) {
|
|
localStorage.setItem('observerLocation', locationObj);
|
|
}
|
|
if (!localStorage.getItem('ais_observerLocation')) {
|
|
localStorage.setItem('ais_observerLocation', locationObj);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Detect location using gpsd (USB GPS) or browser geolocation as fallback
|
|
*/
|
|
function detectLocationGPS(btn) {
|
|
const latInput = document.getElementById('observerLatInput');
|
|
const lonInput = document.getElementById('observerLonInput');
|
|
|
|
// Show loading state with visual feedback
|
|
const originalText = btn.innerHTML;
|
|
btn.innerHTML = '<span class="detecting-spinner"></span> Detecting...';
|
|
btn.disabled = true;
|
|
btn.style.opacity = '0.7';
|
|
|
|
// Helper to restore button state
|
|
function restoreButton() {
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
btn.style.opacity = '';
|
|
}
|
|
|
|
// Helper to set location values
|
|
function setLocation(lat, lon, source) {
|
|
if (latInput) latInput.value = parseFloat(lat).toFixed(4);
|
|
if (lonInput) lonInput.value = parseFloat(lon).toFixed(4);
|
|
restoreButton();
|
|
if (typeof showNotification === 'function') {
|
|
showNotification('Location', `Coordinates set from ${source}`);
|
|
}
|
|
}
|
|
|
|
// First, try gpsd (USB GPS device)
|
|
fetch('/gps/position')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'ok' && data.position && data.position.latitude != null) {
|
|
// Got valid position from gpsd
|
|
setLocation(data.position.latitude, data.position.longitude, 'GPS device');
|
|
} else if (data.status === 'waiting') {
|
|
// gpsd connected but no fix yet - show message and try browser
|
|
if (typeof showNotification === 'function') {
|
|
showNotification('GPS', 'GPS device connected but no fix yet. Trying browser location...');
|
|
}
|
|
useBrowserGeolocation();
|
|
} else {
|
|
// gpsd not available, try browser geolocation
|
|
useBrowserGeolocation();
|
|
}
|
|
})
|
|
.catch(() => {
|
|
// gpsd request failed, try browser geolocation
|
|
useBrowserGeolocation();
|
|
});
|
|
|
|
// Fallback to browser geolocation
|
|
function useBrowserGeolocation() {
|
|
if (!navigator.geolocation) {
|
|
restoreButton();
|
|
if (typeof showNotification === 'function') {
|
|
showNotification('Location', 'No GPS available (gpsd not running, browser GPS unavailable)');
|
|
} else {
|
|
alert('No GPS available');
|
|
}
|
|
return;
|
|
}
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(pos) => {
|
|
setLocation(pos.coords.latitude, pos.coords.longitude, 'browser');
|
|
},
|
|
(err) => {
|
|
restoreButton();
|
|
let message = 'Failed to get location';
|
|
if (err.code === 1) message = 'Location access denied';
|
|
else if (err.code === 2) message = 'Location unavailable';
|
|
else if (err.code === 3) message = 'Location request timed out';
|
|
|
|
if (typeof showNotification === 'function') {
|
|
showNotification('Location', message);
|
|
} else {
|
|
alert(message);
|
|
}
|
|
},
|
|
{ enableHighAccuracy: true, timeout: 10000 }
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save observer location to localStorage
|
|
*/
|
|
function saveObserverLocation() {
|
|
const latInput = document.getElementById('observerLatInput');
|
|
const lonInput = document.getElementById('observerLonInput');
|
|
|
|
const lat = parseFloat(latInput?.value);
|
|
const lon = parseFloat(lonInput?.value);
|
|
|
|
if (isNaN(lat) || lat < -90 || lat > 90) {
|
|
if (typeof showNotification === 'function') {
|
|
showNotification('Location', 'Invalid latitude (must be -90 to 90)');
|
|
} else {
|
|
alert('Invalid latitude (must be -90 to 90)');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (isNaN(lon) || lon < -180 || lon > 180) {
|
|
if (typeof showNotification === 'function') {
|
|
showNotification('Location', 'Invalid longitude (must be -180 to 180)');
|
|
} else {
|
|
alert('Invalid longitude (must be -180 to 180)');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (window.ObserverLocation && ObserverLocation.isSharedEnabled()) {
|
|
ObserverLocation.setShared({ lat, lon });
|
|
} else {
|
|
localStorage.setItem('observerLat', lat.toString());
|
|
localStorage.setItem('observerLon', lon.toString());
|
|
}
|
|
|
|
// Also update dashboard-specific location keys for ADS-B and AIS
|
|
const locationObj = JSON.stringify({ lat: lat, lon: lon });
|
|
localStorage.setItem('observerLocation', locationObj); // ADS-B dashboard
|
|
localStorage.setItem('ais_observerLocation', locationObj); // AIS dashboard
|
|
|
|
// Update display
|
|
const currentLatDisplay = document.getElementById('currentLatDisplay');
|
|
const currentLonDisplay = document.getElementById('currentLonDisplay');
|
|
if (currentLatDisplay) currentLatDisplay.textContent = lat.toFixed(4) + '°';
|
|
if (currentLonDisplay) currentLonDisplay.textContent = lon.toFixed(4) + '°';
|
|
|
|
if (typeof showNotification === 'function') {
|
|
showNotification('Location', 'Observer location saved');
|
|
}
|
|
|
|
if (window.observerLocation) {
|
|
window.observerLocation.lat = lat;
|
|
window.observerLocation.lon = lon;
|
|
}
|
|
|
|
// Refresh SSTV ISS schedule if available
|
|
if (typeof SSTV !== 'undefined' && typeof SSTV.loadIssSchedule === 'function') {
|
|
SSTV.loadIssSchedule();
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Update Settings Functions
|
|
// =============================================================================
|
|
|
|
/**
|
|
* Check for updates manually from settings panel
|
|
*/
|
|
async function checkForUpdatesManual() {
|
|
const content = document.getElementById('updateStatusContent');
|
|
if (!content) return;
|
|
|
|
if (typeof Updater === 'undefined') {
|
|
content.innerHTML = `<div style="color: var(--text-dim); padding: 10px;">Update checking is unavailable. If you use a content blocker, try allowing <code>updater.js</code> to load.</div>`;
|
|
return;
|
|
}
|
|
|
|
content.innerHTML = '<div style="text-align: center; padding: 20px; color: var(--text-dim);">Checking for updates...</div>';
|
|
|
|
try {
|
|
const data = await Updater.checkNow();
|
|
renderUpdateStatus(data);
|
|
} catch (error) {
|
|
content.innerHTML = `<div style="color: var(--accent-red); padding: 10px;">Error checking for updates: ${error.message}</div>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load update status when tab is opened
|
|
*/
|
|
async function loadUpdateStatus() {
|
|
const content = document.getElementById('updateStatusContent');
|
|
if (!content) return;
|
|
|
|
if (typeof Updater === 'undefined') {
|
|
content.innerHTML = `<div style="color: var(--text-dim); padding: 10px;">Update checking is unavailable. If you use a content blocker, try allowing <code>updater.js</code> to load.</div>`;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const data = await Updater.getStatus();
|
|
renderUpdateStatus(data);
|
|
} catch (error) {
|
|
content.innerHTML = `<div style="color: var(--accent-red); padding: 10px;">Error loading update status: ${error.message}</div>`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render update status in settings panel
|
|
*/
|
|
function renderUpdateStatus(data) {
|
|
const content = document.getElementById('updateStatusContent');
|
|
if (!content) return;
|
|
|
|
if (!data.success) {
|
|
content.innerHTML = `<div style="color: var(--accent-red); padding: 10px;">Error: ${data.error || 'Unknown error'}</div>`;
|
|
return;
|
|
}
|
|
|
|
if (data.disabled) {
|
|
content.innerHTML = `
|
|
<div style="padding: 15px; background: var(--bg-tertiary); border-radius: 6px; text-align: center;">
|
|
<div style="color: var(--text-dim); font-size: 13px;">Update checking is disabled</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
if (!data.checked) {
|
|
content.innerHTML = `
|
|
<div style="padding: 15px; background: var(--bg-tertiary); border-radius: 6px; text-align: center;">
|
|
<div style="color: var(--text-dim); font-size: 13px;">No update check performed yet</div>
|
|
<div style="font-size: 11px; color: var(--text-dim); margin-top: 5px;">Click "Check Now" to check for updates</div>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const statusColor = data.update_available ? 'var(--accent-green)' : 'var(--text-dim)';
|
|
const statusText = data.update_available ? 'Update Available' : 'Up to Date';
|
|
const statusIcon = data.update_available
|
|
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>'
|
|
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>';
|
|
|
|
let html = `
|
|
<div style="padding: 15px; background: var(--bg-tertiary); border-radius: 6px; border-left: 3px solid ${statusColor};">
|
|
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 12px;">
|
|
<span style="color: ${statusColor};">${statusIcon}</span>
|
|
<span style="font-weight: 600; color: ${statusColor};">${statusText}</span>
|
|
</div>
|
|
<div style="display: grid; gap: 8px; font-size: 12px;">
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-dim);">Current Version</span>
|
|
<span style="font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; color: var(--text-primary);">v${data.current_version}</span>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-dim);">Latest Version</span>
|
|
<span style="font-family: 'Roboto Condensed', 'Arial Narrow', sans-serif; color: ${data.update_available ? 'var(--accent-green)' : 'var(--text-primary)'};">v${data.latest_version}</span>
|
|
</div>
|
|
${data.last_check ? `
|
|
<div style="display: flex; justify-content: space-between;">
|
|
<span style="color: var(--text-dim);">Last Checked</span>
|
|
<span style="color: var(--text-secondary);">${formatLastCheck(data.last_check)}</span>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
${data.update_available ? `
|
|
<button onclick="Updater.showUpdateModal()" style="
|
|
margin-top: 12px;
|
|
width: 100%;
|
|
padding: 8px;
|
|
background: var(--accent-green);
|
|
color: #000;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
">View Update Details</button>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
|
|
content.innerHTML = html;
|
|
}
|
|
|
|
/**
|
|
* Format last check timestamp
|
|
*/
|
|
function formatLastCheck(isoString) {
|
|
try {
|
|
const date = new Date(isoString);
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
|
|
if (diffMins < 1) return 'Just now';
|
|
if (diffMins < 60) return `${diffMins} min ago`;
|
|
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
|
|
return date.toLocaleDateString();
|
|
} catch (e) {
|
|
return isoString;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle update checking
|
|
*/
|
|
async function toggleUpdateCheck(enabled) {
|
|
// This would require adding a setting to disable update checks
|
|
// For now, just store in localStorage
|
|
localStorage.setItem('intercept_update_check_enabled', enabled ? 'true' : 'false');
|
|
|
|
if (!enabled && typeof Updater !== 'undefined') {
|
|
Updater.destroy();
|
|
} else if (enabled && typeof Updater !== 'undefined') {
|
|
Updater.init();
|
|
}
|
|
}
|
|
|
|
// Extend switchSettingsTab to load update status
|
|
const _originalSwitchSettingsTab = typeof switchSettingsTab !== 'undefined' ? switchSettingsTab : null;
|
|
|
|
function switchSettingsTab(tabName) {
|
|
// Update tab buttons
|
|
document.querySelectorAll('.settings-tab').forEach(tab => {
|
|
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
|
});
|
|
|
|
// Update sections
|
|
document.querySelectorAll('.settings-section').forEach(section => {
|
|
section.classList.toggle('active', section.id === `settings-${tabName}`);
|
|
});
|
|
|
|
// Load content based on tab
|
|
if (tabName === 'tools') {
|
|
loadSettingsTools();
|
|
} else if (tabName === 'updates') {
|
|
loadUpdateStatus();
|
|
} else if (tabName === 'location') {
|
|
loadObserverLocation();
|
|
} else if (tabName === 'alerts') {
|
|
if (typeof AlertCenter !== 'undefined') {
|
|
AlertCenter.loadFeed();
|
|
}
|
|
} else if (tabName === 'recording') {
|
|
if (typeof RecordingUI !== 'undefined') {
|
|
RecordingUI.refresh();
|
|
}
|
|
} else if (tabName === 'apikeys') {
|
|
loadApiKeyStatus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load API key status into the API Keys settings tab
|
|
*/
|
|
function loadApiKeyStatus() {
|
|
const badge = document.getElementById('apiKeyStatusBadge');
|
|
const desc = document.getElementById('apiKeyStatusDesc');
|
|
const usage = document.getElementById('apiKeyUsageCount');
|
|
const bar = document.getElementById('apiKeyUsageBar');
|
|
|
|
if (!badge) return;
|
|
|
|
badge.textContent = 'Not available';
|
|
badge.className = 'asset-badge missing';
|
|
desc.textContent = 'GSM feature removed';
|
|
}
|
|
|
|
/**
|
|
* Save API key from the settings input
|
|
*/
|
|
function saveApiKey() {
|
|
const input = document.getElementById('apiKeyInput');
|
|
const result = document.getElementById('apiKeySaveResult');
|
|
if (!input || !result) return;
|
|
|
|
const key = input.value.trim();
|
|
if (!key) {
|
|
result.style.display = 'block';
|
|
result.style.color = 'var(--accent-red)';
|
|
result.textContent = 'Please enter an API key.';
|
|
return;
|
|
}
|
|
|
|
result.style.display = 'block';
|
|
result.style.color = 'var(--text-dim)';
|
|
result.textContent = 'Saving...';
|
|
|
|
result.style.color = 'var(--accent-red)';
|
|
result.textContent = 'GSM feature has been removed.';
|
|
}
|
|
|
|
/**
|
|
* Toggle API key input visibility
|
|
*/
|
|
function toggleApiKeyVisibility() {
|
|
const input = document.getElementById('apiKeyInput');
|
|
if (!input) return;
|
|
input.type = input.type === 'password' ? 'text' : 'password';
|
|
}
|