diff --git a/static/css/components/signal-cards.css b/static/css/components/signal-cards.css index 283aeb6..2ccc275 100644 --- a/static/css/components/signal-cards.css +++ b/static/css/components/signal-cards.css @@ -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); +} diff --git a/static/js/components/activity-timeline.js b/static/js/components/activity-timeline.js index c0eeeb7..234b104 100644 --- a/static/js/components/activity-timeline.js +++ b/static/js/components/activity-timeline.js @@ -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() {
No activity recorded
-
Events will appear as they are detected
+
Activity will appear here as events are observed
@@ -737,7 +737,7 @@ const ActivityTimeline = (function() {
No activity recorded
-
Events will appear as they are detected
+
Activity will appear here as events are observed
`; } else { diff --git a/static/js/components/signal-cards.js b/static/js/components/signal-cards.js index 1cf5914..c07453e 100644 --- a/static/js/components/signal-cards.js +++ b/static/js/components/signal-cards.js @@ -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() {
APRS - ${escapeHtml(msg.callsign || 'Unknown')} + ${escapeHtml(msg.callsign || 'Unknown')}
${status !== 'baseline' ? ` @@ -833,12 +833,44 @@ const SignalCards = (function() { ? createSignalIndicator(rssi, { compact: true }) : '--'; + // 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 = ` +
+
Signal Identification
+
+
+ `; + } + } + card.innerHTML = `
${escapeHtml(msg.model || 'Unknown')} ID: ${escapeHtml(msg.id || 'N/A')} ${signalIndicator} + ${signalGuessBadge}
${status !== 'baseline' ? ` @@ -913,6 +945,7 @@ const SignalCards = (function() {
${rssi !== null ? createSignalAssessmentPanel(rssi, stats?.lastSeen ? (Date.now() - stats.firstSeen) / 1000 : null, seenCount) : ''} + ${signalGuessSection}
Sensor Details
@@ -953,6 +986,15 @@ const SignalCards = (function() {
`; + // 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 = ` +
+
+
+ + +
+
+
Raw Packet Data
+

+                    
+ +
+ `; + 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 diff --git a/static/js/components/signal-timeline.js b/static/js/components/signal-timeline.js index df08dae..3570801 100644 --- a/static/js/components/signal-timeline.js +++ b/static/js/components/signal-timeline.js @@ -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() {
📡
-
No signals recorded yet
-
Signals will appear as they are detected
+
No signal activity recorded
+
Activity will appear here as signals are observed
@@ -548,8 +548,8 @@ const SignalTimeline = (function() { lanesContainer.innerHTML = `
📡
-
No signals recorded yet
-
Signals will appear as they are detected
+
No signal activity recorded
+
Activity will appear here as signals are observed
`; } else { diff --git a/static/js/core/app.js b/static/js/core/app.js index d4ac23c..1c9bebb 100644 --- a/static/js/core/app.js +++ b/static/js/core/app.js @@ -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 ============== diff --git a/templates/index.html b/templates/index.html index 744b4f9..7ce7efc 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1464,6 +1464,7 @@ +