// ─── DATA ─────────────────────────────────────────────────── let ALL_DATA = []; let filteredData = []; let sortKey = 'date'; let sortDir = -1; // -1 = desc let currentPage = 1; const PAGE_SIZE = 50; let logUnavailable = false; // ─── FULL LOG ──────────────────────────────────────────────── const FULL_LOG_URL = 'data/k5bss-log.json'; async function loadFullLog() { try { const res = await fetch(FULL_LOG_URL); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { console.warn('Could not load full log:', err); return null; } } async function init() { const log = await loadFullLog(); logUnavailable = !log; ALL_DATA = log || []; filteredData = [...ALL_DATA]; if (logUnavailable) { document.getElementById('log-error').hidden = false; renderTable(); return; } // Update stats const countries = new Set(ALL_DATA.map(r => r.country).filter(Boolean)); const states = new Set(ALL_DATA.filter(r => r.country === 'United States').map(r => r.state).filter(Boolean)); document.getElementById('stat-qso').textContent = ALL_DATA.length.toLocaleString(); document.getElementById('stat-dxcc').textContent = countries.size; document.getElementById('stat-states').textContent = states.size; buildDashboard(); populateFilters(); renderTable(); } // ─── DASHBOARD ─────────────────────────────────────────────── function buildDashboard() { // Modes const modes = countBy(ALL_DATA, 'mode'); const topModes = Object.entries(modes).sort((a,b)=>b[1]-a[1]).slice(0,7); const maxMode = topModes[0][1]; const modesEl = document.getElementById('modes-chart'); const modeColors = { FT8:'#ffaa00', FT4:'#ffcc44', DIG:'#ff8800', USB:'#4499ff', LSB:'#66aaff', SSB:'#88bbff', RTTY:'#ff4455' }; modesEl.innerHTML = topModes.map(([m, c]) => `
${m}
${c}
`).join(''); // Bands const bands = countBy(ALL_DATA, 'band'); const topBands = Object.entries(bands).sort((a,b)=>b[1]-a[1]).slice(0,8); const maxBand = topBands[0][1]; const bandEl = document.getElementById('bands-chart'); const bandColors = { '20M':'#4499ff','40M':'#00ff88','80M':'#ff4455','10M':'#00ddff','30M':'#88aaff','17M':'#aaddff','15M':'#ffaaff','12M':'#ffdd88' }; bandEl.innerHTML = topBands.map(([b, c]) => `
${b}
${c}
`).join(''); // Countries const countries = countBy(ALL_DATA, 'country'); const topCountries = Object.entries(countries).sort((a,b)=>b[1]-a[1]).slice(0,20); const ctEl = document.getElementById('countries-list'); ctEl.innerHTML = topCountries.map(([c,n]) => `
${c||'Unknown'} ${n}
`).join(''); // Animate bars after a tick requestAnimationFrame(() => { setTimeout(() => { document.querySelectorAll('.bar-fill').forEach(el => { el.style.width = el.dataset.target + '%'; }); }, 100); }); // Activity chart drawActivityChart(); } function drawActivityChart() { if (!ALL_DATA.length) return; const canvas = document.getElementById('activityCanvas'); const dpr = window.devicePixelRatio || 1; const W = canvas.parentElement.offsetWidth - 48; const H = 120; canvas.width = W * dpr; canvas.height = H * dpr; canvas.style.width = W + 'px'; canvas.style.height = H + 'px'; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); // Group by year-month const monthly = {}; ALL_DATA.forEach(r => { if (!r.date) return; const key = r.date.slice(0,7); monthly[key] = (monthly[key]||0) + 1; }); const keys = Object.keys(monthly).sort(); const vals = keys.map(k => monthly[k]); const max = Math.max(...vals); const pad = { l: 10, r: 10, t: 15, b: 30 }; const chartW = W - pad.l - pad.r; const chartH = H - pad.t - pad.b; const barW = Math.max(2, (chartW / keys.length) - 1); ctx.clearRect(0,0,W,H); // Grid line ctx.strokeStyle = 'rgba(30,37,48,0.8)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(pad.l, pad.t + chartH); ctx.lineTo(W - pad.r, pad.t + chartH); ctx.stroke(); // Bars keys.forEach((k, i) => { const x = pad.l + i * (chartW / keys.length); const h = (vals[i] / max) * chartH; const y = pad.t + chartH - h; const grad = ctx.createLinearGradient(0, y, 0, y + h); grad.addColorStop(0, 'rgba(0,255,136,0.9)'); grad.addColorStop(1, 'rgba(0,255,136,0.15)'); ctx.fillStyle = grad; ctx.fillRect(x, y, barW, h); }); // X labels (every 6 months) ctx.fillStyle = 'rgba(90,112,128,0.9)'; ctx.font = `${9 * dpr / dpr}px 'Share Tech Mono', monospace`; ctx.textAlign = 'center'; keys.forEach((k, i) => { if (i % 6 === 0) { const x = pad.l + i * (chartW / keys.length) + barW/2; ctx.fillText(k, x, H - 8); } }); } // ─── TABLE ─────────────────────────────────────────────────── function populateFilters() { const bands = [...new Set(ALL_DATA.map(r=>r.band).filter(Boolean))].sort(); const modes = [...new Set(ALL_DATA.map(r=>r.mode).filter(Boolean))].sort(); const bandSel = document.getElementById('filter-band'); const modeSel = document.getElementById('filter-mode'); bands.forEach(b => { const o = document.createElement('option'); o.value=b; o.text=b; bandSel.appendChild(o); }); modes.forEach(m => { const o = document.createElement('option'); o.value=m; o.text=m; modeSel.appendChild(o); }); } function applyFilters() { const q = document.getElementById('search-input').value.trim().toLowerCase(); const band = document.getElementById('filter-band').value; const mode = document.getElementById('filter-mode').value; const lotw = document.getElementById('filter-lotw').value; filteredData = ALL_DATA.filter(r => { if (band && r.band !== band) return false; if (mode && r.mode !== mode) return false; if (lotw && r.lotw !== lotw) return false; if (q) { const hay = [r.call, r.name, r.country, r.qth, r.state, r.grid].join(' ').toLowerCase(); if (!hay.includes(q)) return false; } return true; }); currentPage = 1; renderTable(); } function sortTable(key) { if (sortKey === key) sortDir *= -1; else { sortKey = key; sortDir = key === 'date' || key === 'time' ? -1 : 1; } filteredData.sort((a,b) => { let av = a[key]||'', bv = b[key]||''; if (key === 'dist') { av = parseInt(av)||0; bv = parseInt(bv)||0; } if (av < bv) return -sortDir; if (av > bv) return sortDir; return 0; }); // Update header indicators document.querySelectorAll('thead th').forEach(th => { th.classList.remove('sort-asc','sort-desc'); }); const headers = ['call','name','date','time','band','mode','rst_sent','country','dist','lotw']; const idx = headers.indexOf(key); if (idx >= 0) { const ths = document.querySelectorAll('thead th'); ths[idx].classList.add(sortDir === 1 ? 'sort-asc' : 'sort-desc'); } renderTable(); } function renderTable() { const tbody = document.getElementById('log-tbody'); const total = filteredData.length; const pages = Math.ceil(total / PAGE_SIZE); const start = (currentPage - 1) * PAGE_SIZE; const slice = filteredData.slice(start, start + PAGE_SIZE); document.getElementById('log-meta').textContent = `${total.toLocaleString()} QSOs · Page ${currentPage} of ${Math.max(1,pages)}`; if (slice.length === 0) { const msg = logUnavailable ? 'LOG UNAVAILABLE — TRY AGAIN LATER' : 'NO CONTACTS FOUND'; tbody.innerHTML = `${msg}`; } else { tbody.innerHTML = slice.map(r => { const timeStr = r.time ? r.time.slice(0,2)+':'+r.time.slice(2,4) : ''; const bandClass = `band-${r.band}`; const lotwClass = r.lotw === 'Y' ? 'lotw-y' : 'lotw-n'; const lotwSym = r.lotw === 'Y' ? '✓' : '·'; const rst = r.rst_sent && r.rst_rcvd ? `${r.rst_sent} / ${r.rst_rcvd}` : r.rst_sent || ''; return ` ${esc(r.call)} ${esc(r.name ? titleCase(r.name) : '')} ${esc(r.date)} ${timeStr}Z ${esc(r.band)} ${esc(r.mode)} ${esc(rst)} ${esc(r.country||'')}${r.state ? ' '+esc(r.state)+'' : ''} ${r.dist ? Number(r.dist).toLocaleString() : ''} ${lotwSym} `; }).join(''); } renderPagination(pages); } function renderPagination(pages) { const el = document.getElementById('pagination'); if (pages <= 1) { el.innerHTML = ''; return; } let html = ``; const range = []; if (pages <= 7) { for (let i=1;i<=pages;i++) range.push(i); } else { range.push(1); if (currentPage > 3) range.push('...'); for (let i=Math.max(2,currentPage-1); i<=Math.min(pages-1,currentPage+1); i++) range.push(i); if (currentPage < pages-2) range.push('...'); range.push(pages); } range.forEach(p => { if (p === '...') html += `…`; else html += ``; }); html += ``; el.innerHTML = html; } function goPage(p) { currentPage = p; renderTable(); document.querySelector('.table-wrap').scrollIntoView({behavior:'smooth',block:'nearest'}); } // ─── HELPERS ───────────────────────────────────────────────── function countBy(arr, key) { return arr.reduce((acc, r) => { const v = r[key]; if (v) acc[v] = (acc[v]||0)+1; return acc; }, {}); } function esc(s) { return String(s||'').replace(/&/g,'&').replace(//g,'>'); } function titleCase(s) { return s.toLowerCase().replace(/\b\w/g, c => c.toUpperCase()); } function showSection(id, btn) { document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); document.getElementById('section-' + id).classList.add('active'); btn.classList.add('active'); } window.addEventListener('load', init); window.addEventListener('resize', () => { if (document.getElementById('section-dashboard').classList.contains('active')) { drawActivityChart(); } });