diff --git a/static/css/components/signal-cards.css b/static/css/components/signal-cards.css index 2ccc275..16be715 100644 --- a/static/css/components/signal-cards.css +++ b/static/css/components/signal-cards.css @@ -1485,3 +1485,301 @@ .signal-guess-section .signal-guess-popup-explanation { color: var(--text-secondary, #aaa); } + +/* ============================================ + CLICKABLE CARDS + ============================================ */ +.signal-card.signal-card-clickable { + cursor: pointer; + transition: all 0.15s ease; +} + +.signal-card.signal-card-clickable:hover { + border-color: var(--accent-cyan, #00d4ff); + box-shadow: 0 0 0 1px rgba(0, 212, 255, 0.2); +} + +.signal-card.signal-card-clickable:active { + transform: scale(0.995); +} + +/* Floating action buttons for clickable cards */ +.signal-card-actions-float { + position: absolute; + top: 8px; + right: 8px; + display: flex; + gap: 6px; + opacity: 0; + transition: opacity 0.15s ease; + z-index: 2; +} + +.signal-card.signal-card-clickable:hover .signal-card-actions-float { + opacity: 1; +} + +.signal-card-actions-float .signal-action-btn { + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-dim); + background: var(--bg-secondary); + border: 1px solid var(--border-color); + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + transition: all 0.15s; +} + +.signal-card-actions-float .signal-action-btn:hover { + color: var(--text-secondary); + border-color: var(--border-light); + background: var(--bg-tertiary); +} + +/* ============================================ + SIGNAL DETAILS MODAL + ============================================ */ +.signal-details-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transition: opacity 0.2s ease, visibility 0.2s ease; +} + +.signal-details-modal.show { + opacity: 1; + visibility: visible; +} + +.signal-details-modal-backdrop { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(4px); +} + +.signal-details-modal-content { + position: relative; + background: var(--panel-bg, #1a1a2e); + border: 1px solid var(--border-color, #333); + border-radius: 12px; + width: 90%; + max-width: 600px; + max-height: 85vh; + display: flex; + flex-direction: column; + transform: scale(0.95); + transition: transform 0.2s ease; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); +} + +.signal-details-modal.show .signal-details-modal-content { + transform: scale(1); +} + +.signal-details-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid var(--border-color, #333); + background: rgba(0, 0, 0, 0.2); + border-radius: 12px 12px 0 0; +} + +.signal-details-modal-title { + font-family: 'JetBrains Mono', monospace; + font-size: 15px; + font-weight: 600; + color: var(--accent-cyan, #00d4ff); +} + +.signal-details-modal-close { + background: none; + border: none; + color: var(--text-muted, #888); + font-size: 24px; + cursor: pointer; + padding: 0; + line-height: 1; + transition: color 0.15s ease; +} + +.signal-details-modal-close:hover { + color: var(--accent-red, #ff4444); +} + +.signal-details-modal-body { + padding: 20px; + overflow-y: auto; + flex: 1; +} + +.signal-details-modal-footer { + display: flex; + justify-content: flex-end; + padding: 12px 20px; + border-top: 1px solid var(--border-color, #333); + background: rgba(0, 0, 0, 0.2); + border-radius: 0 0 12px 12px; +} + +.signal-details-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; +} + +.signal-details-copy-btn:hover { + background: var(--accent-cyan, #00d4ff); + color: #000; +} + +/* Signal Details Content Sections */ +.signal-details-section { + margin-bottom: 20px; +} + +.signal-details-section:last-child { + margin-bottom: 0; +} + +.signal-details-title { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-dim, #666); + margin-bottom: 10px; + padding-bottom: 6px; + border-bottom: 1px solid var(--border-color, #333); +} + +.signal-details-message { + font-family: 'JetBrains Mono', monospace; + font-size: 14px; + color: var(--text-primary, #e0e0e0); + background: var(--bg-secondary, #252525); + padding: 12px 14px; + border-radius: 6px; + word-break: break-word; + line-height: 1.5; +} + +.signal-details-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.signal-details-item { + display: flex; + flex-direction: column; + gap: 2px; +} + +.signal-details-label { + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-dim, #666); +} + +.signal-details-value { + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + color: var(--text-primary, #e0e0e0); +} + +/* Raw data in modal */ +.signal-details-modal .signal-raw-data { + font-family: 'JetBrains Mono', monospace; + font-size: 11px; + color: var(--text-secondary, #aaa); + background: var(--bg-tertiary, #1a1a1a); + padding: 12px; + border-radius: 6px; + border: 1px solid var(--border-color, #333); + white-space: pre-wrap; + word-break: break-all; + margin: 0; + max-height: 200px; + overflow-y: auto; +} + +/* Signal assessment panel in modal */ +.signal-details-modal .signal-assessment { + background: var(--bg-secondary, #252525); + padding: 14px; + border-radius: 8px; + margin-bottom: 16px; +} + +.signal-details-modal .signal-assessment-summary { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.signal-details-modal .signal-assessment-text { + font-size: 13px; + color: var(--text-secondary, #aaa); + line-height: 1.4; +} + +.signal-details-modal .signal-assessment-caveat { + font-size: 10px; + color: var(--text-dim, #666); + font-style: italic; + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid var(--border-color, #333); +} + +/* Signal guess section in modal */ +.signal-details-modal .signal-guess-section { + background: var(--bg-secondary, #252525); + padding: 14px; + border-radius: 8px; + margin-bottom: 16px; + border: none; +} + +.signal-details-modal .signal-guess-content { + margin-top: 8px; +} + +/* Responsive adjustments */ +@media (max-width: 500px) { + .signal-details-modal-content { + width: 95%; + max-height: 90vh; + } + + .signal-details-grid { + grid-template-columns: 1fr; + } +} diff --git a/static/js/components/signal-cards.js b/static/js/components/signal-cards.js index c07453e..512df66 100644 --- a/static/js/components/signal-cards.js +++ b/static/js/components/signal-cards.js @@ -578,13 +578,16 @@ const SignalCards = (function() { const msgType = getMsgTypeLabel(msg); const card = document.createElement('article'); - card.className = 'signal-card'; + card.className = 'signal-card signal-card-clickable'; card.dataset.status = status; card.dataset.type = 'message'; card.dataset.protocol = protoClass; card.dataset.msgType = msgType.toLowerCase(); if (msg.address) card.dataset.address = msg.address; + // Store message data for dialog + card.dataset.msgData = JSON.stringify(msg); + // Get address stats for display const stats = getAddressStats('pager', msg.address); const seenCount = stats ? stats.count : 1; @@ -610,63 +613,17 @@ const SignalCards = (function() {
${escapeHtml(msg.message || '[No content]')}
- -
-
-
-
-
Signal Details
-
-
- Protocol - ${escapeHtml(msg.protocol)} -
-
- Address - ${escapeHtml(msg.address)} -
- ${msg.function ? ` -
- Function - ${escapeHtml(msg.function)} -
- ` : ''} -
- Type - ${escapeHtml(msgType)} -
-
- Seen - ${seenCount} time${seenCount > 1 ? 's' : ''} -
-
- Timestamp - ${escapeHtml(msg.timestamp)} -
-
-
- ${msg.raw ? ` -
-
Raw Data
-
${escapeHtml(msg.raw)}
-
- ` : ''} -
-
+
+ ${!isToneOnly ? `` : ''} +
`; + // Add click handler to open details dialog + card.addEventListener('click', () => { + showSignalDetails(card); + }); + return card; } @@ -817,12 +774,15 @@ const SignalCards = (function() { const relativeTime = formatRelativeTime(msg.timestamp); const card = document.createElement('article'); - card.className = 'signal-card'; + card.className = 'signal-card signal-card-clickable'; card.dataset.status = status; card.dataset.type = 'sensor'; card.dataset.protocol = msg.model || 'unknown'; if (msg.id) card.dataset.sensorId = msg.id; + // Store message data for dialog + card.dataset.msgData = JSON.stringify(msg); + // Get stats const stats = getAddressStats('sensor', msg.id); const seenCount = stats ? stats.count : 1; @@ -834,12 +794,10 @@ const SignalCards = (function() { : '--'; // 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({ + const signalGuess = SignalGuess.guessSignalType({ frequency_hz: frequencyHz, modulation: msg.modulation || null, bandwidth_hz: msg.bandwidth ? parseFloat(msg.bandwidth) * 1000 : null, @@ -851,17 +809,6 @@ const SignalCards = (function() { 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 = ` -
-
Signal Identification
-
-
- `; - } } card.innerHTML = ` @@ -930,70 +877,15 @@ const SignalCards = (function() { ` : ''}
- -
-
-
- ${rssi !== null ? createSignalAssessmentPanel(rssi, stats?.lastSeen ? (Date.now() - stats.firstSeen) / 1000 : null, seenCount) : ''} - ${signalGuessSection} -
-
Sensor Details
-
-
- Model - ${escapeHtml(msg.model || 'Unknown')} -
-
- ID - ${escapeHtml(msg.id || 'N/A')} -
- ${msg.channel ? ` -
- Channel - ${msg.channel} -
- ` : ''} - ${msg.frequency ? ` -
- Frequency - ${msg.frequency} MHz -
- ` : ''} -
- Seen - ${seenCount} time${seenCount > 1 ? 's' : ''} -
-
-
- ${msg.raw ? ` -
-
Raw Data
-
${escapeHtml(typeof msg.raw === 'object' ? JSON.stringify(msg.raw, null, 2) : msg.raw)}
-
- ` : ''} -
-
+
+
`; - // 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); - } - } + // Add click handler to open details dialog + card.addEventListener('click', () => { + showSignalDetails(card); + }); return card; } @@ -1321,6 +1213,227 @@ const SignalCards = (function() { modal.classList.add('show'); } + /** + * Show signal details dialog for pager/sensor cards + */ + function showSignalDetails(card) { + const type = card.dataset.type; + const msgData = JSON.parse(card.dataset.msgData || '{}'); + + // Create or reuse modal + let modal = document.getElementById('signalDetailsModal'); + if (!modal) { + modal = document.createElement('div'); + modal.id = 'signalDetailsModal'; + modal.className = 'signal-details-modal'; + modal.innerHTML = ` +
+
+
+ + +
+
+ +
+ `; + document.body.appendChild(modal); + + // Close handlers + modal.querySelector('.signal-details-modal-backdrop').addEventListener('click', () => { + modal.classList.remove('show'); + }); + modal.querySelector('.signal-details-modal-close').addEventListener('click', () => { + modal.classList.remove('show'); + }); + modal.querySelector('.signal-details-copy-btn').addEventListener('click', () => { + const rawEl = modal.querySelector('.signal-raw-data'); + if (rawEl) { + navigator.clipboard.writeText(rawEl.textContent).then(() => { + showToast('Raw data copied to clipboard'); + }).catch(() => { + showToast('Failed to copy', 'error'); + }); + } + }); + + // Close on escape key + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && modal.classList.contains('show')) { + modal.classList.remove('show'); + } + }); + } + + // Build content based on card type + let title = ''; + let bodyContent = ''; + + if (type === 'message') { + // Pager message details + title = `${escapeHtml(msgData.protocol || 'Pager')} - Address ${escapeHtml(msgData.address || 'Unknown')}`; + const stats = getAddressStats('pager', msgData.address); + const seenCount = stats ? stats.count : 1; + const msgType = getMsgTypeLabel(msgData); + + bodyContent = ` +
+
Message Content
+
${escapeHtml(msgData.message || '[No content]')}
+
+
+
Signal Details
+
+
+ Protocol + ${escapeHtml(msgData.protocol || 'Unknown')} +
+
+ Address + ${escapeHtml(msgData.address || 'Unknown')} +
+ ${msgData.function ? ` +
+ Function + ${escapeHtml(msgData.function)} +
+ ` : ''} +
+ Type + ${escapeHtml(msgType)} +
+
+ Seen + ${seenCount} time${seenCount > 1 ? 's' : ''} +
+
+ Timestamp + ${escapeHtml(msgData.timestamp || 'Unknown')} +
+
+
+ ${msgData.raw ? ` +
+
Raw Data
+
${escapeHtml(msgData.raw)}
+
+ ` : ''} + `; + } else if (type === 'sensor') { + // 433MHz sensor details + title = `${escapeHtml(msgData.model || 'Sensor')} - ID ${escapeHtml(msgData.id || 'Unknown')}`; + const stats = getAddressStats('sensor', msgData.id); + const seenCount = stats ? stats.count : 1; + const rssi = msgData.rssi || msgData.signal_strength || msgData.snr || msgData.noise || null; + + // Signal assessment section + let signalAssessment = ''; + if (rssi !== null) { + signalAssessment = createSignalAssessmentPanel(rssi, stats?.lastSeen ? (Date.now() - stats.firstSeen) / 1000 : null, seenCount); + } + + // Signal guess section + let signalGuessHtml = ''; + if (msgData.frequency && typeof SignalGuess !== 'undefined') { + const frequencyHz = parseFloat(msgData.frequency) * 1_000_000; + const signalGuess = SignalGuess.guessSignalType({ + frequency_hz: frequencyHz, + modulation: msgData.modulation || null, + bandwidth_hz: msgData.bandwidth ? parseFloat(msgData.bandwidth) * 1000 : null, + rssi_dbm: rssi, + region: 'UK/EU' + }); + if (signalGuess) { + const guessElement = SignalGuess.createGuessElement(signalGuess, { showAlternatives: true, compact: false }); + signalGuessHtml = ` +
+
Signal Identification
+
${guessElement.outerHTML}
+
+ `; + } + } + + // Sensor readings + let sensorReadings = ''; + const readings = []; + if (msgData.temperature !== undefined) readings.push(`
Temperature${msgData.temperature}°${msgData.temperature_unit || 'F'}
`); + if (msgData.humidity !== undefined) readings.push(`
Humidity${msgData.humidity}%
`); + if (msgData.battery !== undefined) readings.push(`
Battery${msgData.battery}
`); + if (msgData.pressure !== undefined) readings.push(`
Pressure${msgData.pressure} ${msgData.pressure_unit || 'hPa'}
`); + if (msgData.wind_speed !== undefined) readings.push(`
Wind Speed${msgData.wind_speed} ${msgData.wind_unit || 'mph'}
`); + if (msgData.rain !== undefined) readings.push(`
Rain${msgData.rain} ${msgData.rain_unit || 'mm'}
`); + if (msgData.state !== undefined) readings.push(`
State${escapeHtml(msgData.state)}
`); + + if (readings.length > 0) { + sensorReadings = ` +
+
Sensor Readings
+
${readings.join('')}
+
+ `; + } + + bodyContent = ` + ${signalAssessment} + ${signalGuessHtml} + ${sensorReadings} +
+
Sensor Details
+
+
+ Model + ${escapeHtml(msgData.model || 'Unknown')} +
+
+ ID + ${escapeHtml(msgData.id || 'N/A')} +
+ ${msgData.channel ? ` +
+ Channel + ${msgData.channel} +
+ ` : ''} + ${msgData.frequency ? ` +
+ Frequency + ${msgData.frequency} MHz +
+ ` : ''} +
+ Seen + ${seenCount} time${seenCount > 1 ? 's' : ''} +
+
+ Timestamp + ${escapeHtml(msgData.timestamp || 'Unknown')} +
+
+
+ ${msgData.raw ? ` +
+
Raw Data
+
${escapeHtml(typeof msgData.raw === 'object' ? JSON.stringify(msgData.raw, null, 2) : msgData.raw)}
+
+ ` : ''} + `; + } + + // Populate modal + modal.querySelector('.signal-details-modal-title').textContent = title; + modal.querySelector('.signal-details-modal-body').innerHTML = bodyContent; + + // Show/hide copy button based on whether there's raw data + const copyBtn = modal.querySelector('.signal-details-copy-btn'); + copyBtn.style.display = (msgData.raw) ? '' : 'none'; + + // Show modal + modal.classList.add('show'); + } + /** * Show toast notification */ @@ -1785,6 +1898,7 @@ const SignalCards = (function() { isAddressMuted, showOnMap, showStationRawData, + showSignalDetails, showToast, // Filter bar