'use client'; import { API_BASE } from '@/lib/api'; import { useState, useEffect, useRef, useCallback } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { RadioReceiver, Activity, Play, Square, FastForward, ChevronDown, ChevronUp, } from 'lucide-react'; import type { DashboardData, SelectedEntity, RadioFeed, SigintSignal } from '@/types/dashboard'; export default function RadioInterceptPanel({ data, isEavesdropping, setIsEavesdropping, eavesdropLocation, cameraCenter, selectedEntity: _selectedEntity, }: { data: DashboardData; isEavesdropping?: boolean; setIsEavesdropping?: (val: boolean) => void; eavesdropLocation?: { lat: number; lng: number } | null; cameraCenter?: { lat: number; lng: number } | null; selectedEntity?: SelectedEntity | null; }) { const [isMinimized, setIsMinimized] = useState(true); const [feeds, setFeeds] = useState([]); const [activeFeed, setActiveFeed] = useState(null); const [isPlaying, setIsPlaying] = useState(false); const [isScanning, setIsScanning] = useState(false); const audioRef = useRef(null); const [volume, setVolume] = useState(0.8); const volumeRef = useRef(volume); const scanTimeoutRef = useRef(null); // Fetch the top feeds on mount useEffect(() => { const fetchFeeds = async () => { try { const res = await fetch(`${API_BASE}/api/radio/top`); if (res.ok) { const json = await res.json(); setFeeds(json); } } catch (e) { console.error('Failed to fetch radio feeds', e); } }; fetchFeeds(); // Refresh every 5 minutes const interval = setInterval(fetchFeeds, 300000); return () => clearInterval(interval); }, []); const playFeed = useCallback((feed: RadioFeed) => { if (isScanning && scanTimeoutRef.current) { clearTimeout(scanTimeoutRef.current); setIsScanning(false); } setActiveFeed(feed); setIsPlaying(true); }, [isScanning]); // Handle Eavesdrop Map Clicks useEffect(() => { if (eavesdropLocation && isEavesdropping) { const fetchNearest = async () => { try { // Show a temporary state setFeeds((prev) => [ { id: 'scanning-nearest', name: 'TRIANGULATING SIGNAL...', location: `LAT:${eavesdropLocation.lat.toFixed(2)} LNG:${eavesdropLocation.lng.toFixed(2)}`, listeners: 0, category: 'SIGINT', }, ...prev, ]); const res = await fetch( `${API_BASE}/api/radio/nearest?lat=${eavesdropLocation.lat}&lng=${eavesdropLocation.lng}`, ); if (res.ok) { const system = await res.json(); if (system && system.shortName) { // Valid OpenMHZ system found! Fetch recent calls const callRes = await fetch( `${API_BASE}/api/radio/openmhz/calls/${system.shortName}`, ); if (callRes.ok) { const calls = await callRes.json(); if (calls && calls.length > 0) { // Found bursts! const latest = calls[0]; const openMhzFeed = { id: `openmhz-${system.shortName}-${latest.id}`, name: `${system.name} (TG:${latest.talkgroupNum})`, location: `${system.city}, ${system.state}`, listeners: system.clientCount || 0, category: 'TRUNKED INTERCEPT', stream_url: latest.url, }; // Remove the triangulating placeholder and add the new intercept setFeeds((prev) => { const clean = prev.filter((f) => f.id !== 'scanning-nearest'); // Avoid duplicates if we clicked the same place twice if (clean.find((f) => f.id === openMhzFeed.id)) return clean; return [openMhzFeed, ...clean]; }); // Auto-play the intercept playFeed(openMhzFeed); } else { // Provide failure feedback setFeeds((prev) => { const clean = prev.filter((f) => f.id !== 'scanning-nearest'); return [ { id: `failed-${Date.now()}`, name: `NO RECENT COMMS (${system.shortName})`, location: `${system.city}, ${system.state}`, category: 'DEAD AIR', listeners: 0, }, ...clean, ]; }); } } } else { // Provide failure feedback setFeeds((prev) => { const clean = prev.filter((f) => f.id !== 'scanning-nearest'); return [ { id: `failed-${Date.now()}`, name: 'NO LOCAL REPEATERS FOUND', location: 'UNKNOWN', category: 'ENCRYPTED / VOID', listeners: 0, }, ...clean, ]; }); } } } catch (e) { console.error('Nearest system lookup failed', e); } }; fetchNearest(); } }, [eavesdropLocation, isEavesdropping, playFeed]); const stopFeed = useCallback(() => { if (isScanning && scanTimeoutRef.current) { clearTimeout(scanTimeoutRef.current); setIsScanning(false); } setActiveFeed(null); setIsPlaying(false); }, [isScanning]); // Handle Audio Element Play/Stop useEffect(() => { if (activeFeed && isPlaying) { if (!audioRef.current) { const audio = new Audio(activeFeed.stream_url || ''); audioRef.current = audio; } else { audioRef.current.src = activeFeed.stream_url || ''; } audioRef.current.volume = volumeRef.current; audioRef.current.play().catch((e) => console.log('Audio play blocked', e)); } else { if (audioRef.current) { audioRef.current.pause(); audioRef.current.src = ''; } } }, [activeFeed, isPlaying]); useEffect(() => { volumeRef.current = volume; if (audioRef.current) { audioRef.current.volume = volume; } }, [volume]); // Cleanup on unmount useEffect(() => { return () => { if (audioRef.current) { audioRef.current.pause(); audioRef.current.src = ''; } if (scanTimeoutRef.current) { clearTimeout(scanTimeoutRef.current); } }; }, []); const toggleScan = () => { if (isScanning) { setIsScanning(false); if (scanTimeoutRef.current) clearTimeout(scanTimeoutRef.current); stopFeed(); } else { setIsScanning(true); scanNextFeed(); } }; const scanNextFeed = async () => { if (!isScanning) return; // Try localized scan first if we have a camera center or eavesdrop location const scanLoc = eavesdropLocation || cameraCenter; let localFeedFound = false; if (scanLoc) { try { const res = await fetch( `${API_BASE}/api/radio/nearest-list?lat=${scanLoc.lat}&lng=${scanLoc.lng}&limit=3`, ); if (res.ok) { const systems = await res.json(); // Try to find a system with an active unplayed burst for (const system of systems) { if (system && system.shortName) { const callRes = await fetch( `${API_BASE}/api/radio/openmhz/calls/${system.shortName}`, ); if (callRes.ok) { const calls = await callRes.json(); if (calls && calls.length > 0) { // Normally we would track played calls. For now just pick random recent one. const randomCall = calls[Math.floor(Math.random() * Math.min(calls.length, 3))]; const openMhzFeed = { id: `openmhz-${system.shortName}-${randomCall.id}`, name: `${system.name} (TG:${randomCall.talkgroupNum})`, location: `${system.city}, ${system.state}`, listeners: system.clientCount || 0, category: 'TRUNKED INTERCEPT', stream_url: randomCall.url, }; // Replace feeds list visually with this active sector setFeeds((prev) => { if (prev.find((f) => f.id === openMhzFeed.id)) return prev; return [openMhzFeed, ...prev].slice(0, 10); }); setActiveFeed(openMhzFeed); setIsPlaying(true); localFeedFound = true; break; } } } } } } catch (e) { console.error('Auto scan local query failed', e); } } if (!localFeedFound && feeds.length > 0) { // Fallback: Pick a random hot feed or cycle them const randomIdx = Math.floor(Math.random() * Math.min(feeds.length, 10)); // Pick from top 10 setActiveFeed(feeds[randomIdx]); setIsPlaying(true); } // Scan for 15 seconds then switch scanTimeoutRef.current = setTimeout(() => { if (isScanning) scanNextFeed(); }, 15000); }; return (
setIsMinimized(!isMinimized)} >
SIGINT INTERCEPT {isPlaying && }
{!isMinimized && ( {/* Audio Player Controls */}
{activeFeed ? activeFeed.name : 'NO SIGNAL'} {activeFeed ? `LOCATION: ${activeFeed.location.toUpperCase()}` : 'AWAITING TUNING...'}
{activeFeed && (
LIVE
)}
setVolume(parseFloat(e.target.value))} className="w-20 accent-cyan-500" title="Volume" />
{/* Fake Waveform Visualizer */}
{Array.from({ length: 48 }).map((_, i) => ( ))}
{/* Feed List */}
{feeds.length === 0 ? (
SEARCHING FREQUENCIES...
) : ( feeds.map((feed: RadioFeed) => (
playFeed(feed)} className={`p-2 mb-1 cursor-pointer border-l-2 ${activeFeed?.id === feed.id ? 'bg-cyan-900/30 border-cyan-400' : 'border-transparent hover:bg-white/5'} flex justify-between items-center transition-colors`} >
{feed.name} {feed.location} | {feed.category}
{feed.listeners.toLocaleString()} LSTN
)) )}
{/* SIGINT Grid Section */} {data?.sigint && data.sigint.length > 0 && (
SIGINT GRID
APRS:{data.sigint.filter((s: SigintSignal) => s.source === 'aprs').length} MESH: {data.sigint.filter((s: SigintSignal) => s.source === 'meshtastic').length} JS8:{data.sigint.filter((s: SigintSignal) => s.source === 'js8call').length}
{data.sigint.slice(0, 25).map((sig: SigintSignal, idx: number) => { const srcColor = sig.source === 'aprs' ? '#22c55e' : sig.source === 'meshtastic' ? '#3b82f6' : '#f59e0b'; // Build a context line from the richest available field const context = sig.status || sig.comment || sig.raw_message?.slice(0, 60) || ''; const stationType = sig.station_type && sig.station_type !== 'Station' ? sig.station_type : ''; const freq = sig.frequency || ''; return (
{ if (sig.lat && sig.lng) { // Dispatch a custom event to fly to this signal window.dispatchEvent( new CustomEvent('flyto', { detail: { lat: sig.lat, lng: sig.lng, zoom: 10 }, }), ); } }} >
{sig.callsign}
{sig.emergency && ( SOS )} {(sig.source || '').toUpperCase()}
{(stationType || freq) && (
{stationType && ( {stationType} )} {freq && ( {freq} )}
)} {context && (

{context.slice(0, 70)}

)}
); })}
)}
)}
); }