mirror of
https://github.com/smittix/intercept.git
synced 2026-06-10 23:13:31 -07:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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">×</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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ==============
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user