fix parsing for dxpedition

pull/27/head
accius 4 days ago
parent 735766a20e
commit f3a61ff9dc

@ -2624,7 +2624,7 @@
fontFamily: 'JetBrains Mono, monospace' fontFamily: 'JetBrains Mono, monospace'
}}> }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: 'var(--accent-amber)', fontWeight: '700' }}>{exp.callsign}</span> <span style={{ color: 'var(--accent-amber)', fontWeight: '700', fontSize: '13px' }}>{exp.callsign}</span>
<span style={{ color: style.color, fontSize: '10px' }}> <span style={{ color: style.color, fontSize: '10px' }}>
{exp.isActive ? '● ACTIVE' : exp.isUpcoming ? 'UPCOMING' : 'PAST'} {exp.isActive ? '● ACTIVE' : exp.isUpcoming ? 'UPCOMING' : 'PAST'}
</span> </span>
@ -2632,9 +2632,12 @@
<div style={{ color: 'var(--text-secondary)', marginTop: '2px' }}> <div style={{ color: 'var(--text-secondary)', marginTop: '2px' }}>
{exp.entity} {exp.entity}
</div> </div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '2px' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '3px' }}>
<span style={{ color: 'var(--text-muted)', fontSize: '11px' }}>{exp.dates}</span> <span style={{ color: 'var(--text-muted)', fontSize: '11px' }}>{exp.dates}</span>
{exp.qsl && <span style={{ color: 'var(--text-muted)', fontSize: '10px' }}>{exp.qsl.substring(0, 15)}</span>} <div style={{ display: 'flex', gap: '6px', fontSize: '10px' }}>
{exp.bands && <span style={{ color: 'var(--accent-purple)' }}>{exp.bands.split(' ').slice(0, 3).join(' ')}</span>}
{exp.modes && <span style={{ color: 'var(--accent-cyan)' }}>{exp.modes.split(' ').slice(0, 2).join(' ')}</span>}
</div>
</div> </div>
</div> </div>
); );
@ -3069,12 +3072,15 @@
paddingLeft: exp.isActive ? '6px' : '0' paddingLeft: exp.isActive ? '6px' : '0'
}}> }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--accent-amber)', fontWeight: '600' }}>{exp.callsign}</span> <span style={{ color: 'var(--accent-amber)', fontWeight: '700' }}>{exp.callsign}</span>
<span style={{ color: exp.isActive ? 'var(--accent-green)' : 'var(--text-muted)', fontSize: '10px' }}> <span style={{ color: exp.isActive ? 'var(--accent-green)' : 'var(--text-muted)', fontSize: '10px' }}>
{exp.isActive ? '● NOW' : ''} {exp.isActive ? '● NOW' : exp.dates?.split('-')[0]?.trim() || ''}
</span> </span>
</div> </div>
<div style={{ color: 'var(--text-muted)', fontSize: '11px' }}>{exp.entity}</div> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: 'var(--text-muted)', fontSize: '11px' }}>{exp.entity}</span>
{exp.bands && <span style={{ color: 'var(--accent-purple)', fontSize: '9px' }}>{exp.bands.split(' ').slice(0,2).join(' ')}</span>}
</div>
</div> </div>
))} ))}
{!dxpeditions.data?.dxpeditions?.length && ( {!dxpeditions.data?.dxpeditions?.length && (
@ -3968,7 +3974,7 @@
</div> </div>
{/* DXpeditions - Compact */} {/* DXpeditions - Compact */}
<div className="panel" style={{ padding: '10px', flex: '0 0 auto', maxHeight: '150px', overflow: 'hidden' }}> <div className="panel" style={{ padding: '10px', flex: '0 0 auto', maxHeight: '160px', overflow: 'hidden' }}>
<div style={{ fontSize: '12px', color: 'var(--accent-cyan)', fontWeight: '700', marginBottom: '6px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ fontSize: '12px', color: 'var(--accent-cyan)', fontWeight: '700', marginBottom: '6px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>🌍 DXPEDITIONS</span> <span>🌍 DXPEDITIONS</span>
{dxpeditions.data && ( {dxpeditions.data && (
@ -3977,22 +3983,29 @@
</span> </span>
)} )}
</div> </div>
<div style={{ overflow: 'auto', maxHeight: '110px', fontSize: '11px', fontFamily: 'JetBrains Mono' }}> <div style={{ overflow: 'auto', maxHeight: '120px', fontSize: '11px', fontFamily: 'JetBrains Mono' }}>
{dxpeditions.data?.dxpeditions?.slice(0, 8).map((exp, i) => ( {dxpeditions.data?.dxpeditions?.slice(0, 8).map((exp, i) => (
<div key={i} style={{ <div key={i} style={{
padding: '3px 0', padding: '4px 0',
borderBottom: '1px solid rgba(255,255,255,0.05)', borderBottom: '1px solid rgba(255,255,255,0.05)',
borderLeft: exp.isActive ? '2px solid var(--accent-green)' : exp.isUpcoming ? '2px solid var(--accent-cyan)' : 'none', borderLeft: exp.isActive ? '2px solid var(--accent-green)' : exp.isUpcoming ? '2px solid var(--accent-cyan)' : 'none',
paddingLeft: (exp.isActive || exp.isUpcoming) ? '6px' : '0' paddingLeft: (exp.isActive || exp.isUpcoming) ? '6px' : '0'
}}> }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: 'var(--accent-amber)', fontWeight: '600' }}>{exp.callsign}</span> <span style={{ color: 'var(--accent-amber)', fontWeight: '700', fontSize: '12px' }}>{exp.callsign}</span>
<span style={{ color: exp.isActive ? 'var(--accent-green)' : 'var(--text-muted)', fontSize: '9px' }}> <span style={{ color: exp.isActive ? 'var(--accent-green)' : 'var(--text-muted)', fontSize: '9px' }}>
{exp.isActive ? '● NOW' : exp.dates?.split('-')[0] || ''} {exp.isActive ? '● NOW' : exp.dates?.split('-')[0]?.trim() || ''}
</span> </span>
</div> </div>
<div style={{ color: 'var(--text-secondary)', fontSize: '10px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '1px' }}>
{exp.entity} <span style={{ color: 'var(--text-secondary)', fontSize: '10px', flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{exp.entity}
</span>
{exp.bands && (
<span style={{ color: 'var(--accent-purple)', fontSize: '9px', marginLeft: '4px' }}>
{exp.bands.split(' ').slice(0, 3).join(' ')}
</span>
)}
</div> </div>
</div> </div>
))} ))}

@ -145,7 +145,7 @@ app.get('/api/solar-indices', async (req, res) => {
} }
}); });
// DXpedition Calendar - fetches from NG3K ADXO // DXpedition Calendar - fetches from NG3K ADXO plain text version
let dxpeditionCache = { data: null, timestamp: 0, maxAge: 30 * 60 * 1000 }; // 30 min cache let dxpeditionCache = { data: null, timestamp: 0, maxAge: 30 * 60 * 1000 }; // 30 min cache
app.get('/api/dxpeditions', async (req, res) => { app.get('/api/dxpeditions', async (req, res) => {
@ -157,98 +157,106 @@ app.get('/api/dxpeditions', async (req, res) => {
return res.json(dxpeditionCache.data); return res.json(dxpeditionCache.data);
} }
// Fetch NG3K ADXO page // Fetch NG3K ADXO plain text version (easier to parse)
const response = await fetch('https://www.ng3k.com/misc/adxo.html'); const response = await fetch('https://www.ng3k.com/Misc/adxoplain.html');
if (!response.ok) throw new Error('Failed to fetch NG3K'); if (!response.ok) throw new Error('Failed to fetch NG3K');
const html = await response.text(); const text = await response.text();
const dxpeditions = []; const dxpeditions = [];
// Parse the HTML table - NG3K uses a specific format // Split by the bullet separator used in the plain text version
// Look for table rows with DXpedition data const entries = text.split(/\s*·\s*/);
const tableMatch = html.match(/<table[^>]*>[\s\S]*?<\/table>/gi);
if (tableMatch) { for (const entry of entries) {
// Find the main data table (usually the largest one) if (!entry.trim() || entry.length < 20) continue;
for (const table of tableMatch) {
const rows = table.match(/<tr[^>]*>[\s\S]*?<\/tr>/gi); // Parse format: "Dec 7, 2025-Jan 5, 2026 DXCC: Guatemala Callsign: TG QSL: LoTW Source: ... Info: ..."
if (!rows || rows.length < 5) continue; // More flexible regex patterns
const dxccMatch = entry.match(/DXCC:\s*([A-Za-z &\-'\.]+?)(?=\s*Callsign:|\s*QSL:|\s*Source:|\s*Info:|$)/i);
const callMatch = entry.match(/Callsign:\s*([A-Z0-9\/]+)/i);
const qslMatch = entry.match(/QSL:\s*([A-Za-z0-9]+)/i);
const infoMatch = entry.match(/Info:\s*(.+)/i);
// Date pattern at the start: "Jan 1, 2026-Feb 16, 2026" or "Jan 1-16, 2026"
const dateMatch = entry.match(/^([A-Za-z]+\s+\d+[^D]*?)(?=\s*DXCC:)/i);
// Must have both DXCC and Callsign to be valid
if (!callMatch || !dxccMatch) continue;
const callsign = callMatch[1].trim().toUpperCase();
const entity = dxccMatch[1].trim();
const qsl = qslMatch ? qslMatch[1].trim() : '';
const info = infoMatch ? infoMatch[1].trim() : '';
const dateStr = dateMatch ? dateMatch[1].trim() : '';
// Skip invalid entries
if (!callsign || callsign.length < 2 || !entity) continue;
// Skip if callsign looks like a date
if (/^\d{4}\s*(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/i.test(callsign)) continue;
// Parse dates
let startDate = null;
let endDate = null;
let isActive = false;
let isUpcoming = false;
// Try to parse dates from dateStr
const monthNames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
const datePattern = /([A-Za-z]+)\s+(\d+)(?:,?\s*(\d{4}))?(?:\s*[-]\s*)?([A-Za-z]+)?\s*(\d+)?(?:,?\s*(\d{4}))?/;
const dateParsed = dateStr.match(datePattern);
if (dateParsed) {
const currentYear = new Date().getFullYear();
for (const row of rows) { const startMonth = monthNames.indexOf(dateParsed[1].toLowerCase().substring(0, 3));
// Skip header rows const startDay = parseInt(dateParsed[2]);
if (row.includes('<th') || row.includes('CALLSIGN') || row.includes('ENTITY')) continue; const startYear = dateParsed[3] ? parseInt(dateParsed[3]) : currentYear;
// Extract cells const endMonthStr = dateParsed[4] || dateParsed[1];
const cells = row.match(/<td[^>]*>([\s\S]*?)<\/td>/gi); const endMonth = monthNames.indexOf(endMonthStr.toLowerCase().substring(0, 3));
if (!cells || cells.length < 4) continue; const endDay = parseInt(dateParsed[5]) || startDay + 14;
const endYear = dateParsed[6] ? parseInt(dateParsed[6]) : startYear;
// Clean cell content
const cleanCell = (cell) => { if (startMonth >= 0) {
return cell startDate = new Date(startYear, startMonth, startDay);
.replace(/<[^>]*>/g, '') // Remove HTML tags endDate = new Date(endYear, endMonth >= 0 ? endMonth : startMonth, endDay);
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/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+)?/); // Handle year rollover for date ranges like "Dec 15 - Jan 5"
if (dateMatch) { if (endDate < startDate && !dateParsed[6]) {
const year = new Date().getFullYear(); endDate.setFullYear(endYear + 1);
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({ const today = new Date();
callsign, today.setHours(0, 0, 0, 0);
entity,
dates, isActive = startDate <= today && endDate >= today;
qsl, isUpcoming = startDate > today;
startDate: startDate?.toISOString(),
endDate: endDate?.toISOString(),
isActive,
isUpcoming
});
} }
} }
// Extract bands and modes from info
const bandsMatch = info.match(/(\d+(?:-\d+)?m)/g);
const bands = bandsMatch ? bandsMatch.join(' ') : '';
const modesMatch = info.match(/\b(CW|SSB|FT8|FT4|RTTY|PSK|FM|AM|DIGI)\b/gi);
const modes = modesMatch ? [...new Set(modesMatch.map(m => m.toUpperCase()))].join(' ') : '';
dxpeditions.push({
callsign,
entity,
dates: dateStr,
qsl,
info: info.substring(0, 100), // Truncate info
bands,
modes,
startDate: startDate?.toISOString(),
endDate: endDate?.toISOString(),
isActive,
isUpcoming
});
} }
// Sort: active first, then upcoming by start date, then past // Sort: active first, then upcoming by start date
dxpeditions.sort((a, b) => { dxpeditions.sort((a, b) => {
if (a.isActive && !b.isActive) return -1; if (a.isActive && !b.isActive) return -1;
if (!a.isActive && b.isActive) return 1; if (!a.isActive && b.isActive) return 1;
@ -259,7 +267,7 @@ app.get('/api/dxpeditions', async (req, res) => {
}); });
const result = { const result = {
dxpeditions: dxpeditions.slice(0, 50), // Limit to 50 entries dxpeditions: dxpeditions.slice(0, 50),
active: dxpeditions.filter(d => d.isActive).length, active: dxpeditions.filter(d => d.isActive).length,
upcoming: dxpeditions.filter(d => d.isUpcoming).length, upcoming: dxpeditions.filter(d => d.isUpcoming).length,
source: 'NG3K ADXO', source: 'NG3K ADXO',

Loading…
Cancel
Save

Powered by TurnKey Linux.