diff --git a/public/index.html b/public/index.html index 7f4c9d7..25fd638 100644 --- a/public/index.html +++ b/public/index.html @@ -1092,6 +1092,37 @@ return { data, loading }; }; + // ============================================ + // DXPEDITION TRACKING HOOK + // ============================================ + const useDXpeditions = () => { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchDXpeditions = async () => { + try { + const response = await fetch('/api/dxpeditions'); + if (response.ok) { + const result = await response.json(); + setData(result); + } + } catch (err) { + console.error('DXpedition fetch error:', err); + } finally { + setLoading(false); + } + }; + + fetchDXpeditions(); + // Refresh every 30 minutes + const interval = setInterval(fetchDXpeditions, 30 * 60 * 1000); + return () => clearInterval(interval); + }, []); + + return { data, loading }; + }; + // ============================================ // SATELLITE TRACKING HOOK // ============================================ @@ -2542,6 +2573,90 @@ ); + // ============================================ + // DXPEDITION PANEL + // ============================================ + const DXpeditionPanel = ({ data, loading }) => { + const formatDate = (isoString) => { + if (!isoString) return ''; + const date = new Date(isoString); + return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + }; + + const getStatusStyle = (expedition) => { + if (expedition.isActive) { + return { bg: 'rgba(0, 255, 136, 0.15)', border: 'var(--accent-green)', color: 'var(--accent-green)' }; + } + if (expedition.isUpcoming) { + return { bg: 'rgba(0, 170, 255, 0.15)', border: 'var(--accent-cyan)', color: 'var(--accent-cyan)' }; + } + return { bg: 'var(--bg-tertiary)', border: 'var(--border-color)', color: 'var(--text-muted)' }; + }; + + return ( +
| ]*>([\s\S]*?)<\/td>/gi); + if (!cells || cells.length < 4) continue; + + // Clean cell content + const cleanCell = (cell) => { + return cell + .replace(/<[^>]*>/g, '') // Remove HTML tags + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .trim(); + }; + + const callsign = cleanCell(cells[0] || ''); + const entity = cleanCell(cells[1] || ''); + const dates = cleanCell(cells[2] || ''); + const qsl = cleanCell(cells[3] || ''); + + // Skip if no valid callsign + if (!callsign || callsign.length < 2 || callsign.includes('CALLSIGN')) continue; + + // Parse dates (format varies: "Jan 15-Feb 28" or "2024 Jan 15-Feb 28") + let startDate = null; + let endDate = null; + let isActive = false; + let isUpcoming = false; + + const dateMatch = dates.match(/(\w+)\s+(\d+)[\s\-]+(\w+)?\s*(\d+)?/); + if (dateMatch) { + const year = new Date().getFullYear(); + const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']; + + const startMonth = monthNames.indexOf(dateMatch[1].toLowerCase().substring(0, 3)); + const startDay = parseInt(dateMatch[2]); + const endMonth = dateMatch[3] ? monthNames.indexOf(dateMatch[3].toLowerCase().substring(0, 3)) : startMonth; + const endDay = parseInt(dateMatch[4]) || startDay + 7; + + if (startMonth >= 0) { + startDate = new Date(year, startMonth, startDay); + endDate = new Date(year, endMonth >= 0 ? endMonth : startMonth, endDay); + + // Handle year rollover + if (endDate < startDate) { + endDate.setFullYear(year + 1); + } + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + isActive = startDate <= today && endDate >= today; + isUpcoming = startDate > today; + } + } + + dxpeditions.push({ + callsign, + entity, + dates, + qsl, + startDate: startDate?.toISOString(), + endDate: endDate?.toISOString(), + isActive, + isUpcoming + }); + } + } + } + + // Sort: active first, then upcoming by start date, then past + dxpeditions.sort((a, b) => { + if (a.isActive && !b.isActive) return -1; + if (!a.isActive && b.isActive) return 1; + if (a.isUpcoming && !b.isUpcoming) return -1; + if (!a.isUpcoming && b.isUpcoming) return 1; + if (a.startDate && b.startDate) return new Date(a.startDate) - new Date(b.startDate); + return 0; + }); + + const result = { + dxpeditions: dxpeditions.slice(0, 50), // Limit to 50 entries + active: dxpeditions.filter(d => d.isActive).length, + upcoming: dxpeditions.filter(d => d.isUpcoming).length, + source: 'NG3K ADXO', + timestamp: new Date().toISOString() + }; + + // Cache the result + dxpeditionCache.data = result; + dxpeditionCache.timestamp = now; + + res.json(result); + } catch (error) { + console.error('DXpedition API error:', error.message); + + // Return cached data if available, even if stale + if (dxpeditionCache.data) { + return res.json({ ...dxpeditionCache.data, stale: true }); + } + + res.status(500).json({ error: 'Failed to fetch DXpedition data' }); + } +}); + // NOAA Space Weather - X-Ray Flux app.get('/api/noaa/xray', async (req, res) => { try { |
|---|