// ─── DATA ─────────────────────────────────────────────────── const RAW_DATA = [{"call":"WE2M","name":"ROBERT A HOGUE","date":"2021-01-24","time":"0553","band":"40M","mode":"FT8","rst_sent":"-20","rst_rcvd":"-10","country":"United States","state":"WA","qth":"E WENATCHEE","lotw":"Y","dist":"2782","grid":"CN97UJ"},{"call":"WD8AXA","name":"CHARLES D AGOSTI","date":"2021-01-24","time":"0555","band":"40M","mode":"FT8","rst_sent":"-07","rst_rcvd":"-11","country":"United States","state":"MI","qth":"PORTAGE","lotw":"N","dist":"1291","grid":"EN72ef"},{"call":"KG4IXS","name":"RICHARD A VAUGHAN","date":"2021-01-24","time":"0558","band":"40M","mode":"FT8","rst_sent":"+00","rst_rcvd":"-13","country":"United States","state":"VA","qth":"CHATHAM","lotw":"Y","dist":"1390","grid":"FM06HU"},{"call":"KE0DC","name":"","date":"2021-01-24","time":"0600","band":"40M","mode":"FT8","rst_sent":"-08","rst_rcvd":"-09","country":"United States","state":"CO","qth":"AURORA","lotw":"Y","dist":"1274","grid":"DM79OQ"},{"call":"KC0HHN","name":"","date":"2021-01-24","time":"0604","band":"40M","mode":"FT8","rst_sent":"-12","rst_rcvd":"-08","country":"United States","state":"IA","qth":"","lotw":"N","dist":"","grid":"EN31"},{"call":"WB0OEW","name":"JAMES D VOGT","date":"2021-01-24","time":"0606","band":"40M","mode":"FT8","rst_sent":"-10","rst_rcvd":"-15","country":"United States","state":"MO","qth":"CAMDENTON","lotw":"Y","dist":"747","grid":"EM37TH"},{"call":"N4JKO","name":"GARY E HORTON","date":"2021-01-24","time":"0608","band":"40M","mode":"FT8","rst_sent":"-12","rst_rcvd":"-15","country":"United States","state":"TN","qth":"","lotw":"N","dist":"","grid":"EM65RT"},{"call":"WW4LL","name":"LESTER P LEEPER","date":"2021-01-24","time":"0610","band":"40M","mode":"FT8","rst_sent":"-14","rst_rcvd":"-13","country":"United States","state":"GA","qth":"BOWDON","lotw":"Y","dist":"575","grid":"EM73DS"},{"call":"K0MBT","name":"MICHAEL B TEDESCO","date":"2021-01-24","time":"0612","band":"40M","mode":"FT8","rst_sent":"-09","rst_rcvd":"-18","country":"United States","state":"MN","qth":"SHAKOPEE","lotw":"Y","dist":"1247","grid":"EN34HE"},{"call":"WB4LHD","name":"ROY A MCDANIEL","date":"2021-01-24","time":"0618","band":"40M","mode":"FT8","rst_sent":"-14","rst_rcvd":"-12","country":"United States","state":"NC","qth":"SANFORD","lotw":"Y","dist":"1118","grid":"FM03HF"},{"call":"N0KJA","name":"FRANK O KORANDA","date":"2021-01-24","time":"0620","band":"40M","mode":"FT8","rst_sent":"-10","rst_rcvd":"-14","country":"United States","state":"KS","qth":"WICHITA","lotw":"Y","dist":"641","grid":"EM17PT"},{"call":"W9JML","name":"JAMES M LAURENDEAU","date":"2021-01-24","time":"0623","band":"40M","mode":"FT8","rst_sent":"-05","rst_rcvd":"-16","country":"United States","state":"IL","qth":"COLLINSVILLE","lotw":"Y","dist":"668","grid":"EM58GI"},{"call":"K9ADV","name":"DWIGHT A VANDERLINDEN","date":"2021-01-24","time":"0625","band":"40M","mode":"FT8","rst_sent":"-11","rst_rcvd":"-13","country":"United States","state":"IN","qth":"INDIANAPOLIS","lotw":"Y","dist":"929","grid":"EM79MF"},{"call":"WB8YYY","name":"ERNEST E MUENZER","date":"2021-01-24","time":"0628","band":"40M","mode":"FT8","rst_sent":"-10","rst_rcvd":"-05","country":"United States","state":"MI","qth":"","lotw":"Y","dist":"","grid":"EN72"},{"call":"KD2UKS","name":"RAYMOND JOSEPH MCDERMOTT","date":"2021-01-24","time":"0641","band":"40M","mode":"FT8","rst_sent":"-12","rst_rcvd":"-15","country":"United States","state":"NY","qth":"MASTIC","lotw":"N","dist":"1561","grid":"FN30HT"},{"call":"KD8IYI","name":"JEFFREY C WHITLEY","date":"2021-01-24","time":"0644","band":"40M","mode":"FT8","rst_sent":"-13","rst_rcvd":"-13","country":"United States","state":"OH","qth":"","lotw":"Y","dist":"","grid":"EM89LG"},{"call":"K9DW","name":"DAN WELBORN","date":"2021-01-24","time":"0647","band":"40M","mode":"FT8","rst_sent":"-09","rst_rcvd":"-06","country":"United States","state":"AL","qth":"DOTHAN","lotw":"Y","dist":"435","grid":"EM64SA"},{"call":"K8DO","name":"DEAN L OSTERMAN","date":"2021-01-24","time":"0649","band":"40M","mode":"FT8","rst_sent":"-11","rst_rcvd":"-10","country":"United States","state":"MI","qth":"ALLENDALE","lotw":"Y","dist":"1298","grid":"EN62OX"},{"call":"K6VMV","name":"VICTOR M VALADEZ","date":"2021-01-24","time":"0651","band":"40M","mode":"FT8","rst_sent":"-08","rst_rcvd":"-12","country":"United States","state":"CA","qth":"MANTECA","lotw":"N","dist":"1954","grid":"CM97KC"},{"call":"W0ULF","name":"MIKE WOLFE","date":"2021-01-24","time":"0653","band":"40M","mode":"FT8","rst_sent":"-11","rst_rcvd":"-13","country":"United States","state":"NE","qth":"OMAHA","lotw":"Y","dist":"1012","grid":"EN21WC"}]; let ALL_DATA = []; let filteredData = []; let sortKey = 'date'; let sortDir = -1; // -1 = desc let currentPage = 1; const PAGE_SIZE = 50; // ─── 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, using sample data:', err); return []; } } async function init() { const fullLog = await loadFullLog(); ALL_DATA = fullLog.length ? fullLog : RAW_DATA; filteredData = [...ALL_DATA]; // 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-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() { 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) { tbody.innerHTML = 'NO CONTACTS FOUND'; } 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(); } });