/** * Spy Stations Mode * Number stations and diplomatic HF radio networks */ const SpyStations = (function() { // State let stations = []; let filteredStations = []; let activeFilters = { types: ['number', 'diplomatic'], countries: [], modes: [] }; // Country flag emoji map const countryFlags = { 'RU': '\u{1F1F7}\u{1F1FA}', 'CU': '\u{1F1E8}\u{1F1FA}', 'BG': '\u{1F1E7}\u{1F1EC}', 'CZ': '\u{1F1E8}\u{1F1FF}', 'EG': '\u{1F1EA}\u{1F1EC}', 'KP': '\u{1F1F0}\u{1F1F5}', 'TN': '\u{1F1F9}\u{1F1F3}', 'US': '\u{1F1FA}\u{1F1F8}', 'PL': '\u{1F1F5}\u{1F1F1}', 'IL': '\u{1F1EE}\u{1F1F1}', 'CN': '\u{1F1E8}\u{1F1F3}', 'MA': '\u{1F1F2}\u{1F1E6}', 'FR': '\u{1F1EB}\u{1F1F7}', 'RO': '\u{1F1F7}\u{1F1F4}', 'DZ': '\u{1F1E9}\u{1F1FF}' }; /** * Initialize the spy stations mode */ function init() { fetchStations(); checkTuneFrequency(); } /** * Fetch stations from the API */ async function fetchStations() { try { const response = await fetch('/spy-stations/stations'); const data = await response.json(); if (data.status === 'success') { stations = data.stations; initFilters(); applyFilters(); updateStats(); } } catch (err) { console.error('Failed to fetch spy stations:', err); } } /** * Initialize filter checkboxes */ function initFilters() { // Get unique countries and modes const countries = [...new Set(stations.map(s => JSON.stringify({name: s.country, code: s.country_code})))].map(s => JSON.parse(s)); const modes = [...new Set(stations.map(s => s.mode.split('/')[0]))].sort(); // Populate country filters const countryContainer = document.getElementById('countryFilters'); if (countryContainer) { countryContainer.innerHTML = countries.map(c => ` `).join(''); } // Populate mode filters const modeContainer = document.getElementById('modeFilters'); if (modeContainer) { modeContainer.innerHTML = modes.map(m => ` `).join(''); } // Set initial filter states activeFilters.countries = countries.map(c => c.code); activeFilters.modes = modes; } /** * Apply filters and render stations */ function applyFilters() { // Read type filters const typeNumber = document.getElementById('filterTypeNumber'); const typeDiplomatic = document.getElementById('filterTypeDiplomatic'); activeFilters.types = []; if (typeNumber && typeNumber.checked) activeFilters.types.push('number'); if (typeDiplomatic && typeDiplomatic.checked) activeFilters.types.push('diplomatic'); // Read country filters activeFilters.countries = []; document.querySelectorAll('#countryFilters input[data-country]:checked').forEach(cb => { activeFilters.countries.push(cb.dataset.country); }); // Read mode filters activeFilters.modes = []; document.querySelectorAll('#modeFilters input[data-mode]:checked').forEach(cb => { activeFilters.modes.push(cb.dataset.mode); }); // Apply filters filteredStations = stations.filter(s => { if (!activeFilters.types.includes(s.type)) return false; if (!activeFilters.countries.includes(s.country_code)) return false; const stationMode = s.mode.split('/')[0]; if (!activeFilters.modes.includes(stationMode)) return false; return true; }); renderStations(); updateStats(true); } /** * Render station cards */ function renderStations() { const container = document.getElementById('spyStationsGrid'); if (!container) return; if (filteredStations.length === 0) { container.innerHTML = `

No stations match your filters

`; return; } container.innerHTML = filteredStations.map(station => renderStationCard(station)).join(''); } /** * Render a single station card */ function renderStationCard(station) { const flag = countryFlags[station.country_code] || ''; const typeBadgeClass = station.type === 'number' ? 'spy-badge-number' : 'spy-badge-diplomatic'; const typeBadgeText = station.type === 'number' ? 'NUMBER' : 'DIPLOMATIC'; const primaryFreq = station.frequencies.find(f => f.primary) || station.frequencies[0]; const freqList = station.frequencies.slice(0, 4).map(f => formatFrequency(f.freq_khz)).join(', '); const moreFreqs = station.frequencies.length > 4 ? ` +${station.frequencies.length - 4} more` : ''; // Build tune button with frequency selector if multiple frequencies let tuneSection; if (station.frequencies.length > 1) { const options = station.frequencies.map(f => { const label = formatFrequency(f.freq_khz) + (f.primary ? ' (primary)' : ''); return ``; }).join(''); tuneSection = `
`; } else { tuneSection = ` `; } return `
${flag} ${station.name} ${station.nickname ? `- ${station.nickname}` : ''}
${typeBadgeText}
Origin ${station.country}
Mode ${station.mode}
Frequencies ${freqList}${moreFreqs}
${station.description}
`; } /** * Format frequency for display */ function formatFrequency(freqKhz) { if (freqKhz >= 1000) { return (freqKhz / 1000).toFixed(3) + ' MHz'; } return freqKhz + ' kHz'; } /** * Get appropriate SDR mode from station mode string */ function getModeFromStation(stationMode) { const mode = stationMode.toLowerCase(); if (mode.includes('am') || mode.includes('ofdm')) return 'am'; if (mode.includes('lsb')) return 'lsb'; if (mode.includes('fm')) return 'fm'; // Default to USB for most number stations and digital modes return 'usb'; } /** * Tune to a station frequency */ function tuneToStation(stationId, freqKhz) { const freqMhz = freqKhz / 1000; // Find the station and determine mode const station = stations.find(s => s.id === stationId); const tuneMode = station ? getModeFromStation(station.mode) : 'usb'; const stationName = station ? station.name : 'Station'; if (typeof showNotification === 'function') { showNotification('Tuning to ' + stationName, formatFrequency(freqKhz) + ' (' + tuneMode.toUpperCase() + ')'); } // Switch to spectrum waterfall mode and tune after mode init. if (typeof switchMode === 'function') { switchMode('waterfall'); } else if (typeof selectMode === 'function') { selectMode('waterfall'); } setTimeout(() => { if (typeof Waterfall !== 'undefined' && typeof Waterfall.quickTune === 'function') { Waterfall.quickTune(freqMhz, tuneMode); } }, 220); } /** * Tune to selected frequency from dropdown */ function tuneToSelectedFreq(stationId) { const select = document.getElementById('freq-select-' + stationId); if (select) { const freqKhz = parseInt(select.value, 10); tuneToStation(stationId, freqKhz); } } /** * Check if we arrived from another page with a tune request */ function checkTuneFrequency() { // Reserved for cross-mode tune handoff behavior. } /** * Show station details modal */ function showDetails(stationId) { const station = stations.find(s => s.id === stationId); if (!station) return; let modal = document.getElementById('spyStationDetailsModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'spyStationDetailsModal'; modal.className = 'signal-details-modal'; document.body.appendChild(modal); } const flag = countryFlags[station.country_code] || ''; const allFreqs = station.frequencies.map(f => { const label = f.primary ? ' (primary)' : ''; return `${formatFrequency(f.freq_khz)}${label}`; }).join(''); modal.innerHTML = `

${flag} ${station.name} ${station.nickname ? '- ' + station.nickname : ''}

Overview
Type ${station.type === 'number' ? 'Number Station' : 'Diplomatic Network'}
Country ${station.country}
Mode ${station.mode}
Operator ${station.operator || 'Unknown'}
Description

${station.description}

Frequencies (${station.frequencies.length})
${allFreqs}
${station.schedule ? `
Schedule

${station.schedule}

` : ''} ${station.source_url ? ` ` : ''}
`; modal.classList.add('show'); } /** * Close details modal */ function closeDetails() { const modal = document.getElementById('spyStationDetailsModal'); if (modal) { modal.classList.remove('show'); } } /** * Show help modal */ function showHelp() { let modal = document.getElementById('spyStationsHelpModal'); if (!modal) { modal = document.createElement('div'); modal.id = 'spyStationsHelpModal'; modal.className = 'signal-details-modal'; document.body.appendChild(modal); } modal.innerHTML = `

About Spy Stations

Number Stations

Number stations are shortwave radio transmissions believed to be used by intelligence agencies to communicate with spies in the field. They typically broadcast strings of numbers, letters, or words read by synthesized or live voices. These one-way broadcasts are encrypted using one-time pads, making them virtually unbreakable.

Diplomatic Networks

Foreign ministries maintain HF radio networks to communicate with embassies worldwide, especially in regions where satellite or internet connectivity may be unreliable or compromised. These networks use various digital modes like PACTOR, ALE, and proprietary protocols for encrypted diplomatic traffic.

How to Listen

Click "Tune In" on any station to open Spectrum Waterfall with the frequency pre-configured. Most number stations use USB (Upper Sideband) mode. You'll need an SDR capable of receiving HF frequencies (typically 3-30 MHz) and an appropriate antenna.

Best Practices
  • HF propagation varies with time of day and solar conditions
  • Use a long wire or loop antenna for best results
  • Check schedules on priyom.org for transmission times
  • Night time generally offers better long-distance reception
Data Sources

Station data sourced from priyom.org, a community-maintained database of number stations and related transmissions.

`; modal.classList.add('show'); } /** * Close help modal */ function closeHelp() { const modal = document.getElementById('spyStationsHelpModal'); if (modal) { modal.classList.remove('show'); } } /** * Update sidebar stats * @param {boolean} useFiltered - If true, use filtered stations instead of all stations */ function updateStats(useFiltered) { const stationList = useFiltered ? filteredStations : stations; const numberCount = stationList.filter(s => s.type === 'number').length; const diplomaticCount = stationList.filter(s => s.type === 'diplomatic').length; const countryCount = new Set(stationList.map(s => s.country_code)).size; const freqCount = stationList.reduce((sum, s) => sum + s.frequencies.length, 0); const numberEl = document.getElementById('spyStatsNumber'); const diplomaticEl = document.getElementById('spyStatsDiplomatic'); const countriesEl = document.getElementById('spyStatsCountries'); const freqsEl = document.getElementById('spyStatsFreqs'); if (numberEl) numberEl.textContent = numberCount; if (diplomaticEl) diplomaticEl.textContent = diplomaticCount; if (countriesEl) countriesEl.textContent = countryCount; if (freqsEl) freqsEl.textContent = freqCount; // Update visible count in header if element exists const visibleCountEl = document.getElementById('spyStationsVisibleCount'); if (visibleCountEl) { visibleCountEl.textContent = stationList.length; } } /** * Destroy — no-op placeholder for consistent lifecycle interface. */ function destroy() { // SpyStations has no background timers or streams to clean up. } // Public API return { init, applyFilters, tuneToStation, tuneToSelectedFreq, showDetails, closeDetails, showHelp, closeHelp, destroy }; })(); // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', function() { // Will be initialized when mode is switched to spy stations });