// ─── 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]) => `
`).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]) => `
`).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();
}
});