Add clickable station badges and integrate signal guessing engine

- Add clickable APRS station badges that display raw packet data in a modal
- Integrate SignalGuess into sensor mode cards for frequency identification
- Standardize UI language across timeline and signal components
- Update frequency band naming for consistency (e.g., "Wi-Fi 2.4GHz" → "2.4 GHz wireless band")

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-01-20 23:59:08 +00:00
parent 3e3bc0e857
commit 42f2a6ef62
6 changed files with 373 additions and 144 deletions
+216
View File
@@ -1269,3 +1269,219 @@
color: var(--text-dim);
font-style: italic;
}
/* ============================================
Clickable Station Badge (APRS)
============================================ */
.signal-station-clickable {
cursor: pointer;
transition: all 0.15s ease;
position: relative;
}
.signal-station-clickable:hover {
background: var(--accent-purple);
color: #000;
transform: scale(1.05);
box-shadow: 0 0 8px rgba(138, 43, 226, 0.4);
}
.signal-station-clickable:active {
transform: scale(0.98);
}
.signal-station-clickable::after {
content: '';
position: absolute;
bottom: -2px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 2px;
background: var(--accent-purple);
transition: width 0.2s ease;
}
.signal-station-clickable:hover::after {
width: 80%;
}
/* ============================================
Station Raw Data Modal
============================================ */
.station-raw-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.station-raw-modal.show {
opacity: 1;
visibility: visible;
}
.station-raw-modal-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
}
.station-raw-modal-content {
position: relative;
background: var(--panel-bg, #1a1a2e);
border: 1px solid var(--border-color, #333);
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
transform: scale(0.95);
transition: transform 0.2s ease;
}
.station-raw-modal.show .station-raw-modal-content {
transform: scale(1);
}
.station-raw-modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color, #333);
background: rgba(0, 0, 0, 0.2);
border-radius: 8px 8px 0 0;
}
.station-raw-modal-title {
font-family: 'JetBrains Mono', monospace;
font-size: 14px;
font-weight: 600;
color: var(--accent-cyan, #00d4ff);
}
.station-raw-modal-close {
background: none;
border: none;
color: var(--text-muted, #888);
font-size: 24px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
transition: color 0.15s ease;
}
.station-raw-modal-close:hover {
color: var(--accent-red, #ff4444);
}
.station-raw-modal-body {
padding: 16px;
overflow-y: auto;
flex: 1;
}
.station-raw-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-muted, #888);
margin-bottom: 8px;
}
.station-raw-data-display {
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 1.6;
color: var(--accent-green, #00ff88);
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--border-color, #333);
border-radius: 4px;
padding: 12px;
word-break: break-all;
white-space: pre-wrap;
margin: 0;
max-height: 300px;
overflow-y: auto;
}
.station-raw-modal-footer {
display: flex;
justify-content: flex-end;
padding: 12px 16px;
border-top: 1px solid var(--border-color, #333);
background: rgba(0, 0, 0, 0.2);
border-radius: 0 0 8px 8px;
}
.station-raw-copy-btn {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
padding: 8px 16px;
background: var(--accent-purple, #8a2be2);
border: none;
border-radius: 4px;
color: #fff;
cursor: pointer;
transition: all 0.15s ease;
}
.station-raw-copy-btn:hover {
background: var(--accent-cyan, #00d4ff);
color: #000;
}
/* ============================================
SIGNAL GUESS INTEGRATION
============================================ */
/* Signal guess badge in card header */
.signal-card-badges .signal-guess-badge {
margin-left: 4px;
}
/* Signal guess section in advanced panel */
.signal-guess-section {
border-bottom: 1px solid var(--border-color);
padding-bottom: 12px;
margin-bottom: 12px;
}
.signal-guess-section .signal-guess-container {
margin-top: 8px;
}
/* Adjust guess label colors for dark theme */
.signal-guess-section .signal-guess-label {
color: var(--text-primary, #e0e0e0);
}
.signal-guess-section .signal-guess-tag {
background: var(--bg-tertiary, #2a2a2a);
color: var(--text-secondary, #888);
}
.signal-guess-section .signal-guess-alt-item {
color: var(--text-secondary, #999);
}
.signal-guess-section .signal-guess-popup-explanation {
color: var(--text-secondary, #aaa);
}
+14 -14
View File
@@ -179,15 +179,15 @@ const ActivityTimeline = (function() {
if (mode === 'rf' || mode === 'tscm' || mode === 'listening-post') {
const f = parseFloat(id);
if (!isNaN(f)) {
if (f >= 2400 && f <= 2500) return 'Wi-Fi 2.4GHz';
if (f >= 5150 && f <= 5850) return 'Wi-Fi 5GHz';
if (f >= 433 && f <= 434) return '433MHz ISM';
if (f >= 868 && f <= 869) return '868MHz ISM';
if (f >= 902 && f <= 928) return '915MHz ISM';
if (f >= 2400 && f <= 2500) return '2.4 GHz wireless band';
if (f >= 5150 && f <= 5850) return '5 GHz wireless band';
if (f >= 433 && f <= 434) return '433 MHz low-power band';
if (f >= 868 && f <= 869) return '868 MHz low-power band';
if (f >= 902 && f <= 928) return '915 MHz low-power band';
if (f >= 315 && f <= 316) return '315MHz';
if (f >= 2402 && f <= 2480) return 'Bluetooth';
if (f >= 144 && f <= 148) return 'VHF Ham';
if (f >= 420 && f <= 450) return 'UHF Ham';
if (f >= 2402 && f <= 2480) return 'Bluetooth band';
if (f >= 144 && f <= 148) return 'VHF amateur band';
if (f >= 420 && f <= 450) return 'UHF amateur band';
return `${f.toFixed(3)} MHz`;
}
}
@@ -231,7 +231,7 @@ const ActivityTimeline = (function() {
item.firstSeen = now;
state.items.set(id, item);
addAnnotation(state, 'new', `New: ${item.label}`, now);
addAnnotation(state, 'new', `New activity: ${item.label}`, now);
}
// Add event
@@ -341,7 +341,7 @@ const ActivityTimeline = (function() {
if (item.pattern !== patternStr) {
item.pattern = patternStr;
addAnnotation(state, 'pattern', `Pattern: ${patternStr} - ${item.label}`, Date.now());
addAnnotation(state, 'pattern', `Repeating pattern observed: ${patternStr} - ${item.label}`, Date.now());
}
}
}
@@ -376,7 +376,7 @@ const ActivityTimeline = (function() {
item.status = item.flagged ? 'flagged' : 'new';
addAnnotation(state,
'flagged',
item.flagged ? `Flagged: ${item.label}` : `Unflagged: ${item.label}`,
item.flagged ? `Marked for review: ${item.label}` : `Review mark removed: ${item.label}`,
Date.now()
);
@@ -398,7 +398,7 @@ const ActivityTimeline = (function() {
const item = state.items.get(id);
if (item && item.status !== 'gone') {
item.status = 'gone';
addAnnotation(state, 'gone', `Inactive: ${item.label}`, Date.now());
addAnnotation(state, 'gone', `No longer active: ${item.label}`, Date.now());
}
}
@@ -479,7 +479,7 @@ const ActivityTimeline = (function() {
<div class="activity-timeline-empty">
<div class="activity-timeline-empty-icon">◯</div>
<div>No activity recorded</div>
<div style="margin-top: 4px; font-size: 9px;">Events will appear as they are detected</div>
<div style="margin-top: 4px; font-size: 9px;">Activity will appear here as events are observed</div>
</div>
</div>
<div class="activity-timeline-annotations" style="display: none;"></div>
@@ -737,7 +737,7 @@ const ActivityTimeline = (function() {
<div class="activity-timeline-empty">
<div class="activity-timeline-empty-icon">◯</div>
<div>No activity recorded</div>
<div style="margin-top: 4px; font-size: 9px;">Events will appear as they are detected</div>
<div style="margin-top: 4px; font-size: 9px;">Activity will appear here as events are observed</div>
</div>
`;
} else {
+124 -26
View File
@@ -26,8 +26,8 @@ const SignalCards = (function() {
STRENGTH_INFO: {
minimal: {
label: 'Minimal',
description: 'At detection threshold',
interpretation: 'may be ambient noise or distant source',
description: 'Near minimum observable level',
interpretation: 'may represent background activity or a distant source',
confidence: 'low',
color: '#888888',
icon: 'signal-0',
@@ -35,8 +35,8 @@ const SignalCards = (function() {
},
weak: {
label: 'Weak',
description: 'Detectable signal',
interpretation: 'potentially distant or obstructed',
description: 'Low-level signal present',
interpretation: 'possibly distant or partially obstructed',
confidence: 'low',
color: '#6baed6',
icon: 'signal-1',
@@ -44,7 +44,7 @@ const SignalCards = (function() {
},
moderate: {
label: 'Moderate',
description: 'Consistent presence',
description: 'Consistent signal presence',
interpretation: 'likely in proximity',
confidence: 'medium',
color: '#3182bd',
@@ -53,8 +53,8 @@ const SignalCards = (function() {
},
strong: {
label: 'Strong',
description: 'Clear signal',
interpretation: 'probable close proximity',
description: 'Clear, consistent signal',
interpretation: 'suggests relatively close proximity',
confidence: 'medium',
color: '#fd8d3c',
icon: 'signal-3',
@@ -62,8 +62,8 @@ const SignalCards = (function() {
},
very_strong: {
label: 'Very Strong',
description: 'High signal level',
interpretation: 'indicates likely nearby source',
description: 'Elevated signal level',
interpretation: 'consistent with a nearby source',
confidence: 'high',
color: '#e6550d',
icon: 'signal-4',
@@ -82,23 +82,23 @@ const SignalCards = (function() {
DURATION_INFO: {
transient: {
label: 'Transient',
modifier: 'briefly observed',
confidence_impact: 'reduces confidence'
modifier: 'observed briefly',
confidence_impact: 'limits assessment confidence'
},
short: {
label: 'Short-duration',
modifier: 'observed for a short period',
confidence_impact: 'limited confidence'
confidence_impact: 'provides limited confidence'
},
sustained: {
label: 'Sustained',
modifier: 'observed over sustained period',
confidence_impact: 'supports confidence'
confidence_impact: 'supports assessment confidence'
},
persistent: {
label: 'Persistent',
modifier: 'continuously observed',
confidence_impact: 'increases confidence'
confidence_impact: 'strengthens assessment confidence'
}
},
@@ -190,11 +190,11 @@ const SignalCards = (function() {
const confidence = this.calculateConfidence(rssi, durationSeconds, observationCount);
if (confidence === 'high') {
return `${strengthInfo.label}, ${durationInfo.label.toLowerCase()} signal with characteristics that suggest device presence in proximity`;
return `${strengthInfo.label}, ${durationInfo.label.toLowerCase()} signal with characteristics that suggest a transmitting device may be nearby`;
} else if (confidence === 'medium') {
return `${strengthInfo.label}, ${durationInfo.label.toLowerCase()} signal that may indicate device activity`;
return `${strengthInfo.label}, ${durationInfo.label.toLowerCase()} signal that may indicate nearby device activity`;
} else {
return `${durationInfo.modifier.charAt(0).toUpperCase() + durationInfo.modifier.slice(1)} ${strengthInfo.label.toLowerCase()} signal consistent with possible device presence`;
return `${durationInfo.modifier.charAt(0).toUpperCase() + durationInfo.modifier.slice(1)} ${strengthInfo.label.toLowerCase()} signal consistent with possible nearby device activity`;
}
},
@@ -207,11 +207,11 @@ const SignalCards = (function() {
const base = strengthInfo.interpretation;
if (confidence === 'high') {
return `Observed signal characteristics suggest ${base}`;
return `Signal characteristics suggest ${base}`;
} else if (confidence === 'medium') {
return `Signal pattern may indicate ${base}`;
return `Observed pattern may indicate ${base}`;
} else {
return `Limited data; signal could represent ${base} or environmental factors`;
return `With limited data, this signal may represent ${base} or environmental factors`;
}
},
@@ -246,7 +246,7 @@ const SignalCards = (function() {
estimate,
rangeMin,
rangeMax,
disclaimer: 'Range estimates are approximate and affected by walls, interference, and transmit power'
disclaimer: 'Range estimates are approximate and influenced by physical obstructions, interference, and transmitter power'
};
}
};
@@ -702,7 +702,7 @@ const SignalCards = (function() {
<div class="signal-card-header">
<div class="signal-card-badges">
<span class="signal-proto-badge aprs">APRS</span>
<span class="signal-freq-badge">${escapeHtml(msg.callsign || 'Unknown')}</span>
<span class="signal-freq-badge signal-station-clickable" data-callsign="${escapeHtml(msg.callsign || 'Unknown')}" data-raw="${msg.raw ? escapeHtml(msg.raw) : ''}" onclick="SignalCards.showStationRawData(this)" title="Click to view raw data">${escapeHtml(msg.callsign || 'Unknown')}</span>
</div>
${status !== 'baseline' ? `
<span class="signal-status-pill" data-status="${status}">
@@ -833,12 +833,44 @@ const SignalCards = (function() {
? createSignalIndicator(rssi, { compact: true })
: '<span class="signal-strength-indicator compact no-data" title="No signal data available">--</span>';
// Signal type guessing based on frequency
let signalGuess = null;
let signalGuessBadge = '';
let signalGuessSection = '';
if (msg.frequency && typeof SignalGuess !== 'undefined') {
const frequencyHz = parseFloat(msg.frequency) * 1_000_000; // Convert MHz to Hz
signalGuess = SignalGuess.guessSignalType({
frequency_hz: frequencyHz,
modulation: msg.modulation || null,
bandwidth_hz: msg.bandwidth ? parseFloat(msg.bandwidth) * 1000 : null,
rssi_dbm: rssi,
region: 'UK/EU'
});
// Create compact badge for header
if (signalGuess && signalGuess.primary_label !== 'Unknown Signal') {
signalGuessBadge = SignalGuess.createCompactBadge(signalGuess).outerHTML;
}
// Create detailed section for advanced panel
if (signalGuess) {
const guessElement = SignalGuess.createGuessElement(signalGuess, { showAlternatives: true, compact: false });
signalGuessSection = `
<div class="signal-advanced-section signal-guess-section">
<div class="signal-advanced-title">Signal Identification</div>
<div class="signal-guess-content"></div>
</div>
`;
}
}
card.innerHTML = `
<div class="signal-card-header">
<div class="signal-card-badges">
<span class="signal-proto-badge sensor">${escapeHtml(msg.model || 'Unknown')}</span>
<span class="signal-freq-badge">ID: ${escapeHtml(msg.id || 'N/A')}</span>
${signalIndicator}
${signalGuessBadge}
</div>
${status !== 'baseline' ? `
<span class="signal-status-pill" data-status="${status}">
@@ -913,6 +945,7 @@ const SignalCards = (function() {
<div class="signal-advanced-inner">
<div class="signal-advanced-content">
${rssi !== null ? createSignalAssessmentPanel(rssi, stats?.lastSeen ? (Date.now() - stats.firstSeen) / 1000 : null, seenCount) : ''}
${signalGuessSection}
<div class="signal-advanced-section">
<div class="signal-advanced-title">Sensor Details</div>
<div class="signal-advanced-grid">
@@ -953,6 +986,15 @@ const SignalCards = (function() {
</div>
`;
// Populate signal guess content if available
if (signalGuess) {
const guessContentDiv = card.querySelector('.signal-guess-content');
if (guessContentDiv) {
const guessElement = SignalGuess.createGuessElement(signalGuess, { showAlternatives: true, compact: false });
guessContentDiv.appendChild(guessElement);
}
}
return card;
}
@@ -1178,9 +1220,9 @@ const SignalCards = (function() {
const message = card.querySelector('.signal-message');
if (message) {
navigator.clipboard.writeText(message.textContent).then(() => {
showToast('Message copied to clipboard');
showToast('Content copied');
}).catch(() => {
showToast('Failed to copy', 'error');
showToast('Unable to copy content', 'error');
});
}
}
@@ -1193,7 +1235,7 @@ const SignalCards = (function() {
if (!muted.includes(address)) {
muted.push(address);
localStorage.setItem('mutedAddresses', JSON.stringify(muted));
showToast(`Address ${address} muted`);
showToast(`Source ${address} hidden from view`);
// Hide existing cards with this address
document.querySelectorAll(`.signal-card[data-address="${address}"], .signal-card[data-callsign="${address}"], .signal-card[data-sensor-id="${address}"]`).forEach(card => {
@@ -1221,7 +1263,62 @@ const SignalCards = (function() {
detail: { lat, lon, label }
});
document.dispatchEvent(event);
showToast(`Showing ${label} on map`);
showToast(`Displaying ${label} location`);
}
/**
* Show raw data modal for a station
*/
function showStationRawData(element) {
const callsign = element.dataset.callsign || 'Unknown';
const rawData = element.dataset.raw || '';
// Create or reuse modal
let modal = document.getElementById('stationRawDataModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'stationRawDataModal';
modal.className = 'station-raw-modal';
modal.innerHTML = `
<div class="station-raw-modal-backdrop"></div>
<div class="station-raw-modal-content">
<div class="station-raw-modal-header">
<span class="station-raw-modal-title"></span>
<button class="station-raw-modal-close">&times;</button>
</div>
<div class="station-raw-modal-body">
<div class="station-raw-label">Raw Packet Data</div>
<pre class="station-raw-data-display"></pre>
</div>
<div class="station-raw-modal-footer">
<button class="station-raw-copy-btn">Copy to Clipboard</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Close handlers
modal.querySelector('.station-raw-modal-backdrop').addEventListener('click', () => {
modal.classList.remove('show');
});
modal.querySelector('.station-raw-modal-close').addEventListener('click', () => {
modal.classList.remove('show');
});
modal.querySelector('.station-raw-copy-btn').addEventListener('click', () => {
const rawText = modal.querySelector('.station-raw-data-display').textContent;
navigator.clipboard.writeText(rawText).then(() => {
showToast('Raw data copied to clipboard');
}).catch(() => {
showToast('Failed to copy', 'error');
});
});
}
// Populate modal
modal.querySelector('.station-raw-modal-title').textContent = `Station: ${callsign}`;
modal.querySelector('.station-raw-data-display').textContent = rawData || 'No raw data available';
// Show modal
modal.classList.add('show');
}
/**
@@ -1687,6 +1784,7 @@ const SignalCards = (function() {
muteAddress,
isAddressMuted,
showOnMap,
showStationRawData,
showToast,
// Filter bar
+18 -18
View File
@@ -61,15 +61,15 @@ const SignalTimeline = (function() {
*/
function categorizeFrequency(freq) {
const f = parseFloat(freq);
if (f >= 2400 && f <= 2500) return 'Wi-Fi 2.4GHz';
if (f >= 5150 && f <= 5850) return 'Wi-Fi 5GHz';
if (f >= 433 && f <= 434) return '433MHz ISM';
if (f >= 868 && f <= 869) return '868MHz ISM';
if (f >= 902 && f <= 928) return '915MHz ISM';
if (f >= 2400 && f <= 2500) return '2.4 GHz wireless band';
if (f >= 5150 && f <= 5850) return '5 GHz wireless band';
if (f >= 433 && f <= 434) return '433 MHz low-power band';
if (f >= 868 && f <= 869) return '868 MHz low-power band';
if (f >= 902 && f <= 928) return '915 MHz low-power band';
if (f >= 315 && f <= 316) return '315MHz';
if (f >= 2402 && f <= 2480) return 'Bluetooth';
if (f >= 144 && f <= 148) return 'VHF Ham';
if (f >= 420 && f <= 450) return 'UHF Ham';
if (f >= 2402 && f <= 2480) return 'Bluetooth band';
if (f >= 144 && f <= 148) return 'VHF amateur band';
if (f >= 420 && f <= 450) return 'UHF amateur band';
return `${freq} MHz`;
}
@@ -86,7 +86,7 @@ const SignalTimeline = (function() {
state.signals.set(frequency, signal);
// Add annotation for new signal
addAnnotation('new', `New signal detected: ${signal.name}`, now);
addAnnotation('new', `New signal observed: ${signal.name}`, now);
}
// Add event
@@ -152,7 +152,7 @@ const SignalTimeline = (function() {
if (signal.status !== 'burst') {
signal.status = 'burst';
addAnnotation('burst',
`Burst: ${recentEvents.length} transmissions in ${config.burstWindow/1000}s - ${signal.name}`,
`Activity cluster: ${recentEvents.length} events in ${config.burstWindow/1000}s - ${signal.name}`,
now
);
}
@@ -200,7 +200,7 @@ const SignalTimeline = (function() {
if (signal.pattern !== patternStr) {
signal.pattern = patternStr;
addAnnotation('pattern',
`Pattern detected: ${patternStr} - ${signal.name}`,
`Repeating pattern observed: ${patternStr} - ${signal.name}`,
Date.now()
);
}
@@ -235,8 +235,8 @@ const SignalTimeline = (function() {
signal.status = signal.flagged ? 'flagged' : 'new';
addAnnotation('flagged',
signal.flagged
? `Flagged for investigation: ${signal.name}`
: `Unflagged: ${signal.name}`,
? `Marked for review: ${signal.name}`
: `Review mark removed: ${signal.name}`,
Date.now()
);
}
@@ -249,7 +249,7 @@ const SignalTimeline = (function() {
const signal = state.signals.get(frequency);
if (signal && signal.status !== 'gone') {
signal.status = 'gone';
addAnnotation('gone', `Signal disappeared: ${signal.name}`, Date.now());
addAnnotation('gone', `Signal no longer observed: ${signal.name}`, Date.now());
}
}
@@ -313,8 +313,8 @@ const SignalTimeline = (function() {
<div class="signal-timeline-lanes" id="timelineLanes">
<div class="signal-timeline-empty">
<div class="signal-timeline-empty-icon">📡</div>
<div>No signals recorded yet</div>
<div style="margin-top: 4px; font-size: 9px;">Signals will appear as they are detected</div>
<div>No signal activity recorded</div>
<div style="margin-top: 4px; font-size: 9px;">Activity will appear here as signals are observed</div>
</div>
</div>
<div class="signal-timeline-annotations" id="timelineAnnotations" style="display: none;"></div>
@@ -548,8 +548,8 @@ const SignalTimeline = (function() {
lanesContainer.innerHTML = `
<div class="signal-timeline-empty">
<div class="signal-timeline-empty-icon">📡</div>
<div>No signals recorded yet</div>
<div style="margin-top: 4px; font-size: 9px;">Signals will appear as they are detected</div>
<div>No signal activity recorded</div>
<div style="margin-top: 4px; font-size: 9px;">Activity will appear here as signals are observed</div>
</div>
`;
} else {
-86
View File
@@ -381,92 +381,6 @@ function showError(text) {
output.insertBefore(errorEl, output.firstChild);
}
// ============== OBSERVER LOCATION ==============
function saveObserverLocation() {
const lat = parseFloat(document.getElementById('adsbObsLat')?.value || document.getElementById('obsLat')?.value);
const lon = parseFloat(document.getElementById('adsbObsLon')?.value || document.getElementById('obsLon')?.value);
if (!isNaN(lat) && !isNaN(lon)) {
observerLocation = { lat, lon };
localStorage.setItem('observerLocation', JSON.stringify(observerLocation));
// Sync both input sets
const adsbLat = document.getElementById('adsbObsLat');
const adsbLon = document.getElementById('adsbObsLon');
const satLat = document.getElementById('obsLat');
const satLon = document.getElementById('obsLon');
if (adsbLat) adsbLat.value = lat.toFixed(4);
if (adsbLon) adsbLon.value = lon.toFixed(4);
if (satLat) satLat.value = lat.toFixed(4);
if (satLon) satLon.value = lon.toFixed(4);
}
}
function useGeolocation() {
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
observerLocation = { lat, lon };
localStorage.setItem('observerLocation', JSON.stringify(observerLocation));
// Update all input fields
const adsbLat = document.getElementById('adsbObsLat');
const adsbLon = document.getElementById('adsbObsLon');
const satLat = document.getElementById('obsLat');
const satLon = document.getElementById('obsLon');
if (adsbLat) adsbLat.value = lat.toFixed(4);
if (adsbLon) adsbLon.value = lon.toFixed(4);
if (satLat) satLat.value = lat.toFixed(4);
if (satLon) satLon.value = lon.toFixed(4);
showInfo(`Location set to ${lat.toFixed(4)}, ${lon.toFixed(4)}`);
},
(error) => {
showError('Geolocation failed: ' + error.message);
}
);
} else {
showError('Geolocation not supported by browser');
}
}
// ============== EXPORT FUNCTIONS ==============
function exportCSV() {
if (allMessages.length === 0) {
alert('No messages to export');
return;
}
const headers = ['Timestamp', 'Protocol', 'Address', 'Function', 'Type', 'Message'];
const csv = [headers.join(',')];
allMessages.forEach(msg => {
const row = [
msg.timestamp || '',
msg.protocol || '',
msg.address || '',
msg.function || '',
msg.msg_type || '',
'"' + (msg.message || '').replace(/"/g, '""') + '"'
];
csv.push(row.join(','));
});
downloadFile(csv.join('\n'), 'intercept_messages.csv', 'text/csv');
}
function exportJSON() {
if (allMessages.length === 0) {
alert('No messages to export');
return;
}
downloadFile(JSON.stringify(allMessages, null, 2), 'intercept_messages.json', 'application/json');
}
// ============== INITIALIZATION ==============
// ============== MOBILE NAVIGATION ==============
+1
View File
@@ -1464,6 +1464,7 @@
<script src="{{ url_for('static', filename='js/core/utils.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/audio.js') }}"></script>
<script src="{{ url_for('static', filename='js/components/radio-knob.js') }}"></script>
<script src="{{ url_for('static', filename='js/components/signal-guess.js') }}"></script>
<script src="{{ url_for('static', filename='js/components/signal-cards.js') }}"></script>
<script src="{{ url_for('static', filename='js/components/signal-timeline.js') }}"></script>
<script src="{{ url_for('static', filename='js/components/activity-timeline.js') }}"></script>