/* =================================================== JAESWIFT.XYZ — LEADERBOARDS FRONTEND Fetches /api/leaderboards and renders 6 panels. =================================================== */ (function () { 'use strict'; const REFRESH_MS = 60000; const API_URL = '/api/leaderboards'; let visitorIpPrefix = null; // first octet of viewer IP for self-highlight let lastFetchedAt = null; // ─── Utils ───────────────────────────────────── function $(sel, root) { return (root || document).querySelector(sel); } function el(tag, attrs, children) { const e = document.createElement(tag); if (attrs) Object.keys(attrs).forEach(k => { if (k === 'class') e.className = attrs[k]; else if (k === 'text') e.textContent = attrs[k]; else if (k === 'html') e.innerHTML = attrs[k]; else e.setAttribute(k, attrs[k]); }); if (Array.isArray(children)) children.forEach(c => c && e.appendChild(c)); return e; } function rank(i) { const n = String(i).padStart(2, '0'); let cls = 'lb-rank'; if (i === 1) cls += ' lb-rank-1'; else if (i === 2) cls += ' lb-rank-2'; else if (i === 3) cls += ' lb-rank-3'; return el('span', { class: cls, text: '#' + n }); } function fmtNum(n) { return (n || 0).toLocaleString('en-GB'); } function ccToFlag(cc) { if (!cc || cc.length !== 2) return ''; const base = 0x1F1E6; const up = cc.toUpperCase(); const a = up.charCodeAt(0) - 65; const b = up.charCodeAt(1) - 65; if (a < 0 || a > 25 || b < 0 || b > 25) return ''; return String.fromCodePoint(base + a) + String.fromCodePoint(base + b); } function secondsAgo(iso) { if (!iso) return '—'; const t = new Date(iso).getTime(); const s = Math.floor((Date.now() - t) / 1000); if (s < 60) return s + 's ago'; if (s < 3600) return Math.floor(s / 60) + 'm ago'; if (s < 86400) return Math.floor(s / 3600) + 'h ago'; return Math.floor(s / 86400) + 'd ago'; } // ─── Renderers ───────────────────────────────── function renderBarRows(panelId, rows, opts) { opts = opts || {}; const panel = document.getElementById(panelId); if (!panel) return; const body = panel.querySelector('.lb-panel-body'); body.innerHTML = ''; if (!rows || !rows.length) { body.appendChild(el('div', { class: 'lb-loading', text: '(no data yet)' })); return; } const max = Math.max.apply(null, rows.map(r => r.count || 0)) || 1; rows.forEach((r, i) => { const pct = Math.max(2, Math.round((r.count / max) * 100)); const row = el('div', { class: 'lb-row' + (r.isSelf ? ' lb-row-self' : '') }); row.appendChild(rank(i + 1)); const labelEl = el('div', { class: 'lb-label' }); if (opts.labelHTML) labelEl.innerHTML = opts.labelHTML(r); else labelEl.textContent = r.label || ''; row.appendChild(labelEl); const countEl = el('span', { class: 'lb-count', text: fmtNum(r.count) + (opts.countSuffix || '') }); row.appendChild(countEl); const barWrap = el('div', { class: 'lb-bar-wrap' }); const bar = el('div', { class: 'lb-bar' }); barWrap.appendChild(bar); row.appendChild(barWrap); body.appendChild(row); // animate width requestAnimationFrame(() => { bar.style.width = pct + '%'; }); }); } function renderCountries(list) { const rows = (list || []).map(c => ({ label: (c.country_name || c.country_code || 'Unknown'), country_code: c.country_code, count: c.count, })); renderBarRows('panelCountries', rows.slice(0, 15), { labelHTML: function (r) { const flag = ccToFlag(r.country_code); return '' + (flag || '🏳️') + '' + escapeHtml(r.label) + ' ' + (r.country_code || '—') + ''; } }); } function renderPages(list) { const rows = (list || []).slice(0, 20).map(p => ({ label: p.path || '/', count: p.count, })); renderBarRows('panelPages', rows, { labelHTML: function (r) { return '' + escapeHtml(r.label) + ''; } }); } function renderReferrers(list) { const rows = (list || []).slice(0, 10).map(r => ({ label: (r.referrer || 'direct'), count: r.count, })); if (!rows.length) { const panel = document.getElementById('panelReferrers'); const body = panel.querySelector('.lb-panel-body'); body.innerHTML = '
(mostly direct traffic — no referrers tracked yet)
'; return; } renderBarRows('panelReferrers', rows, { labelHTML: function (r) { const u = r.label; return '' + escapeHtml(u.length > 48 ? u.slice(0, 48) + '…' : u) + ''; } }); } function renderBrowsers(list) { const rows = (list || []).slice(0, 10).map(b => ({ label: b.browser || 'Other', count: b.count, })); renderBarRows('panelBrowsers', rows, { labelHTML: function (r) { const icons = { 'Chrome': '🟢', 'Firefox': '🦊', 'Safari': '🧭', 'Edge': '🌀', 'Opera': '🔴', 'Bot': '🤖' }; const ic = icons[r.label] || '💠'; return '' + ic + '' + escapeHtml(r.label); } }); } function renderPeakHours(hours) { const panel = document.getElementById('panelHours'); const body = panel.querySelector('.lb-panel-body'); body.innerHTML = ''; if (!hours || !hours.length) { body.appendChild(el('div', { class: 'lb-loading', text: '(no data yet)' })); return; } // Normalise to 24-bucket array const counts = new Array(24).fill(0); hours.forEach(h => { const idx = (typeof h.hour === 'number') ? h.hour : parseInt(h.hour, 10); if (!isNaN(idx) && idx >= 0 && idx < 24) counts[idx] = h.count || 0; }); const max = Math.max.apply(null, counts) || 1; const peakIdx = counts.indexOf(max); const chart = el('div', { class: 'lb-hours' }); counts.forEach((c, i) => { const h = Math.max(2, Math.round((c / max) * 100)); const col = el('div', { class: 'lb-hour-col', 'data-hour': String(i).padStart(2, '0'), 'data-showlabel': (i % 3 === 0) ? '1' : '0', 'data-peak': (i === peakIdx) ? '1' : '0', title: String(i).padStart(2, '0') + ':00 UTC — ' + fmtNum(c) + ' requests' }); col.style.height = h + '%'; chart.appendChild(col); }); body.appendChild(chart); const summary = el('div', { class: 'lb-footer', style: 'margin-top:10px; padding:8px 0; text-align:left; border:none;', html: 'PEAK: ' + String(peakIdx).padStart(2, '0') + ':00 UTC — ' + fmtNum(max) + ' req · Total: ' + fmtNum(counts.reduce((a,b)=>a+b,0)) + ' req/24h' }); body.appendChild(summary); } function renderOperators(list) { const panel = document.getElementById('panelOperators'); const body = panel.querySelector('.lb-panel-body'); body.innerHTML = ''; if (!list || !list.length) { body.appendChild(el('div', { class: 'lb-loading', text: '(no data yet)' })); return; } const max = Math.max.apply(null, list.map(o => o.count || 0)) || 1; list.slice(0, 10).forEach((op, i) => { const isSelf = visitorIpPrefix && op.ip_masked && op.ip_masked.startsWith(visitorIpPrefix + '.'); const row = el('div', { class: 'lb-row' + (isSelf ? ' lb-row-self' : '') }); row.appendChild(rank(i + 1)); const lbl = el('div', { class: 'lb-label' }); lbl.innerHTML = '' + escapeHtml(op.ip_masked) + '' + (isSelf ? ' (YOU)' : '') + ' · last seen ' + escapeHtml(op.last_seen || '—') + ''; row.appendChild(lbl); row.appendChild(el('span', { class: 'lb-count', text: fmtNum(op.count) + ' req' })); const barWrap = el('div', { class: 'lb-bar-wrap' }); const bar = el('div', { class: 'lb-bar' }); if (isSelf) bar.style.background = 'linear-gradient(90deg, var(--warning, #c9a227), var(--status-green, #00cc33))'; barWrap.appendChild(bar); row.appendChild(barWrap); body.appendChild(row); const pct = Math.max(2, Math.round((op.count / max) * 100)); requestAnimationFrame(() => { bar.style.width = pct + '%'; }); }); } function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); } // ─── Main fetch/update ────────────────────────── async function loadSelfIp() { try { const r = await fetch('/api/visitor/scan'); if (!r.ok) return; const d = await r.json(); if (d.ip_masked) { visitorIpPrefix = d.ip_masked.split('.')[0]; // first octet } } catch (e) { /* ignore */ } } async function fetchAndRender() { try { const r = await fetch(API_URL, { cache: 'no-store' }); if (!r.ok) throw new Error('HTTP ' + r.status); const d = await r.json(); lastFetchedAt = new Date(); document.getElementById('lbReqs24h').textContent = '24h REQUESTS: ' + fmtNum(d.total_requests_24h); document.getElementById('lbReqs7d').textContent = '7d REQUESTS: ' + fmtNum(d.total_requests_7d); renderCountries(d.top_countries); renderPages(d.top_pages); renderReferrers(d.top_referrers); renderPeakHours(d.peak_hours); renderBrowsers(d.browsers); renderOperators(d.top_operators); updateUpdatedLabel(); } catch (e) { console.error('leaderboards fetch failed:', e); document.getElementById('lbUpdated').textContent = '● FETCH ERROR — retrying...'; } } function updateUpdatedLabel() { if (!lastFetchedAt) return; const s = Math.floor((Date.now() - lastFetchedAt.getTime()) / 1000); document.getElementById('lbUpdated').textContent = '● LAST UPDATED ' + (s < 5 ? 'just now' : s + 's ago'); } // ─── Init ────────────────────────────────────── document.addEventListener('DOMContentLoaded', async function () { await loadSelfIp(); await fetchAndRender(); setInterval(fetchAndRender, REFRESH_MS); setInterval(updateUpdatedLabel, 1000); const btn = document.getElementById('lbRefresh'); if (btn) btn.addEventListener('click', function () { btn.disabled = true; btn.textContent = '↻ REFRESHING…'; fetchAndRender().then(() => { setTimeout(() => { btn.disabled = false; btn.textContent = '↻ REFRESH'; }, 400); }); }); }); })();