From 735766a20e3912524a53c29696a91e1c59d9fbcb Mon Sep 17 00:00:00 2001 From: accius Date: Sat, 31 Jan 2026 18:33:27 -0500 Subject: [PATCH] dxpedition update --- public/index.html | 194 +++++++++++++++++++++++++++++++++++++++++++++- server.js | 138 +++++++++++++++++++++++++++++++++ 2 files changed, 331 insertions(+), 1 deletion(-) 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 ( +
+
+ 🌍 DXPEDITIONS +
+ {loading &&
} + {data && ( + + {data.active > 0 && {data.active} active} + {data.active > 0 && data.upcoming > 0 && ' • '} + {data.upcoming > 0 && {data.upcoming} upcoming} + + )} +
+
+ +
+ {data?.dxpeditions?.length > 0 ? ( + data.dxpeditions.slice(0, 15).map((exp, idx) => { + const style = getStatusStyle(exp); + return ( +
+
+ {exp.callsign} + + {exp.isActive ? '● ACTIVE' : exp.isUpcoming ? 'UPCOMING' : 'PAST'} + +
+
+ {exp.entity} +
+
+ {exp.dates} + {exp.qsl && {exp.qsl.substring(0, 15)}} +
+
+ ); + }) + ) : ( +
+ {loading ? 'Loading DXpeditions...' : 'No DXpedition data available'} +
+ )} +
+ + {data && ( +
+ + NG3K ADXO Calendar + +
+ )} +
+ ); + }; + // ============================================ // CONTEST CALENDAR PANEL // ============================================ @@ -2635,7 +2750,7 @@ const LegacyLayout = ({ config, currentTime, utcTime, utcDate, localTime, localDate, deGrid, dxGrid, deSunTimes, dxSunTimes, dxLocation, onDXChange, - spaceWeather, bandConditions, solarIndices, potaSpots, dxCluster, dxPaths, contests, propagation, mySpots, satellites, + spaceWeather, bandConditions, solarIndices, potaSpots, dxCluster, dxPaths, dxpeditions, contests, propagation, mySpots, satellites, localWeather, use12Hour, onTimeFormatToggle, onSettingsClick, onFullscreenToggle, isFullscreen, mapLayers, toggleDXPaths, togglePOTA, toggleSatellites @@ -2936,6 +3051,40 @@
+ {/* DXpeditions */} +
+
+ 🌍 DXPEDITIONS + {dxpeditions.data?.active > 0 && ( + {dxpeditions.data.active} active + )} +
+
+ {dxpeditions.data?.dxpeditions?.slice(0, 6).map((exp, i) => ( +
+
+ {exp.callsign} + + {exp.isActive ? '● NOW' : ''} + +
+
{exp.entity}
+
+ ))} + {!dxpeditions.data?.dxpeditions?.length && ( +
+ {dxpeditions.loading ? 'Loading...' : 'No data'} +
+ )} +
+
+ {/* Contests */}
🏆 CONTESTS
@@ -3508,6 +3657,7 @@ const potaSpots = usePOTASpots(); const dxCluster = useDXCluster(config.dxClusterSource || 'auto'); const dxPaths = useDXPaths(); + const dxpeditions = useDXpeditions(); const contests = useContests(); const propagation = usePropagation(config.location, dxLocation); const mySpots = useMySpots(config.callsign); @@ -3588,6 +3738,7 @@ potaSpots={potaSpots} dxCluster={dxCluster} dxPaths={dxPaths} + dxpeditions={dxpeditions} contests={contests} propagation={propagation} mySpots={mySpots} @@ -3816,6 +3967,47 @@
+ {/* DXpeditions - Compact */} +
+
+ 🌍 DXPEDITIONS + {dxpeditions.data && ( + + {dxpeditions.data.active > 0 && {dxpeditions.data.active} active} + + )} +
+
+ {dxpeditions.data?.dxpeditions?.slice(0, 8).map((exp, i) => ( +
+
+ {exp.callsign} + + {exp.isActive ? '● NOW' : exp.dates?.split('-')[0] || ''} + +
+
+ {exp.entity} +
+
+ ))} + {(!dxpeditions.data?.dxpeditions || dxpeditions.data.dxpeditions.length === 0) && +
{dxpeditions.loading ? 'Loading...' : 'No DXpeditions'}
+ } +
+
+ + NG3K ADXO + +
+
+ {/* POTA - Compact */}
diff --git a/server.js b/server.js index 2d07df9..e129af3 100644 --- a/server.js +++ b/server.js @@ -145,6 +145,144 @@ app.get('/api/solar-indices', async (req, res) => { } }); +// DXpedition Calendar - fetches from NG3K ADXO +let dxpeditionCache = { data: null, timestamp: 0, maxAge: 30 * 60 * 1000 }; // 30 min cache + +app.get('/api/dxpeditions', async (req, res) => { + try { + const now = Date.now(); + + // Return cached data if fresh + if (dxpeditionCache.data && (now - dxpeditionCache.timestamp) < dxpeditionCache.maxAge) { + return res.json(dxpeditionCache.data); + } + + // Fetch NG3K ADXO page + const response = await fetch('https://www.ng3k.com/misc/adxo.html'); + if (!response.ok) throw new Error('Failed to fetch NG3K'); + + const html = await response.text(); + const dxpeditions = []; + + // Parse the HTML table - NG3K uses a specific format + // Look for table rows with DXpedition data + const tableMatch = html.match(/]*>[\s\S]*?<\/table>/gi); + + if (tableMatch) { + // Find the main data table (usually the largest one) + for (const table of tableMatch) { + const rows = table.match(/]*>[\s\S]*?<\/tr>/gi); + if (!rows || rows.length < 5) continue; + + for (const row of rows) { + // Skip header rows + if (row.includes(']*>([\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 {