From 2336285644ef1426b9dde8b85eefab8054182658 Mon Sep 17 00:00:00 2001 From: jae Date: Mon, 20 Apr 2026 01:39:14 +0000 Subject: [PATCH] feat(hq): LEADERBOARDS page - 6-panel live rankings (countries/pages/referrers/hours/browsers/operators) + nav link --- api/data/changelog.json | 21 +++ api/data/navigation.json | 5 + css/leaderboards.css | 265 ++++++++++++++++++++++++++++++++++++ hq/leaderboards.html | 116 ++++++++++++++++ js/leaderboards.js | 283 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 690 insertions(+) create mode 100644 css/leaderboards.css create mode 100644 hq/leaderboards.html create mode 100644 js/leaderboards.js diff --git a/api/data/changelog.json b/api/data/changelog.json index d0a563a..da016f0 100644 --- a/api/data/changelog.json +++ b/api/data/changelog.json @@ -1,6 +1,27 @@ { "site": "jaeswift.xyz", "entries": [ + { + "version": "1.37.2", + "date": "20/04/2026", + "category": "FEATURE", + "title": "LEADERBOARDS β€” Live Operational Rankings", + "changes": [ + "New page /hq/leaderboards with 6-panel grid showing real-time traffic intelligence pulled from nginx access logs", + "TOP COUNTRIES (24h): GeoIP-resolved top 15 origins with auto-generated flag emojis and animated ranked bars", + "TOP PAGES (24h): most-hit routes (API/CSS/JS/favicons filtered out), top 20 with green-code path display", + "TOP REFERRERS (7d): inbound link sources filtered for own domain β€” graceful empty state for direct-traffic sites", + "PEAK HOURS (24h UTC): 24-bar hourly chart with amber-highlighted peak column, hover tooltips, auto-summary showing peak hour + total req/24h", + "BROWSER BREAKDOWN (24h): top 10 UA families with per-browser emoji (🟒 Chrome / 🦊 Firefox / 🧭 Safari / πŸŒ€ Edge / πŸ€– Bot)", + "OPERATOR LEADERBOARD (7d): top 10 most-active IPs (privacy-masked first/last octet), request counts, last-seen relative timestamps β€” self-IP highlighted with amber (YOU) badge", + "Gold/silver/bronze rank styling for top-3 rows across all panels; #01–#NN zero-padded ranks elsewhere", + "60s auto-refresh + manual refresh button + live LAST UPDATED counter (ticks per second)", + "Backend /api/leaderboards endpoint returns JSON aggregating countries, pages, referrers, peak_hours, browsers, top_operators β€” 60s in-memory cache", + "New files: hq/leaderboards.html Β· css/leaderboards.css Β· js/leaderboards.js", + "Nav updated: HQ menu now includes LEADERBOARDS link with description \"Live traffic rankings & operator stats\"", + "Fully mobile responsive (768px stack / 420px compact) with scanlines + grid-bg matching site theme" + ] + }, { "version": "1.37.1", "date": "20/04/2026", diff --git a/api/data/navigation.json b/api/data/navigation.json index 1cbd5ec..e04a171 100644 --- a/api/data/navigation.json +++ b/api/data/navigation.json @@ -23,6 +23,11 @@ "label": "MAINTENANCE LOG", "url": "/hq/logs", "description": "Operational logs & diagnostics" + }, + { + "label": "LEADERBOARDS", + "url": "/hq/leaderboards", + "description": "Live traffic rankings & operator stats" } ] }, diff --git a/css/leaderboards.css b/css/leaderboards.css new file mode 100644 index 0000000..4a5a444 --- /dev/null +++ b/css/leaderboards.css @@ -0,0 +1,265 @@ +/* =================================================== + JAESWIFT.XYZ β€” LEADERBOARDS + =================================================== */ + +.lb-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + margin-top: 18px; + font-family: var(--font-mono); + font-size: 11px; +} + +.lb-badge { + padding: 6px 12px; + background: var(--bg-panel, #161616); + border: 1px solid var(--border-color, #2a2a2a); + color: var(--text-secondary, #aaa); + letter-spacing: 1.5px; + text-transform: uppercase; + border-radius: 2px; +} +.lb-badge.lb-live { + color: var(--status-green, #00cc33); + border-color: rgba(0, 204, 51, 0.3); + box-shadow: 0 0 10px rgba(0, 204, 51, 0.15); +} + +.lb-refresh-btn { + padding: 6px 14px; + background: transparent; + border: 1px solid var(--status-green, #00cc33); + color: var(--status-green, #00cc33); + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 1.5px; + cursor: pointer; + text-transform: uppercase; + transition: all 0.2s; + margin-left: auto; +} +.lb-refresh-btn:hover { + background: rgba(0, 204, 51, 0.1); + box-shadow: 0 0 12px rgba(0, 204, 51, 0.4); +} + +/* ─── Grid ─── */ +.lb-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); + gap: 18px; + margin-top: 28px; +} + +.lb-panel { + background: var(--bg-panel, #161616); + border: 1px solid var(--border-color, #2a2a2a); + border-radius: 2px; + overflow: hidden; + position: relative; +} +.lb-panel::before { + content: ''; + position: absolute; + top: 0; left: 0; width: 3px; height: 100%; + background: linear-gradient(180deg, var(--status-green, #00cc33) 0%, transparent 100%); + opacity: 0.6; +} + +.lb-panel-header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + background: rgba(0, 0, 0, 0.35); + border-bottom: 1px solid var(--border-color, #2a2a2a); +} +.lb-panel-icon { font-size: 16px; } +.lb-panel-header h2 { + flex: 1; + margin: 0; + font-family: var(--font-display, 'Share Tech Mono'), 'Orbitron', monospace; + font-size: 13px; + letter-spacing: 2px; + color: var(--text-primary, #e0e0e0); + text-transform: uppercase; +} +.lb-panel-period { + font-family: var(--font-mono); + font-size: 10px; + color: var(--warning, #c9a227); + letter-spacing: 1.5px; + padding: 2px 8px; + border: 1px solid rgba(201, 162, 39, 0.3); + border-radius: 2px; +} + +.lb-panel-body { + padding: 14px 16px; + font-family: var(--font-mono); + min-height: 220px; + max-height: 420px; + overflow-y: auto; +} +.lb-loading { + color: var(--text-secondary, #888); + font-size: 12px; + text-align: center; + padding: 40px 0; + letter-spacing: 1px; + opacity: 0.65; + animation: lb-pulse 1.4s ease-in-out infinite; +} +@keyframes lb-pulse { + 0%,100% { opacity: 0.45; } + 50% { opacity: 0.85; } +} + +/* ─── Rows (generic bar chart row) ─── */ +.lb-row { + display: grid; + grid-template-columns: 36px 1fr auto; + gap: 10px; + align-items: center; + padding: 7px 0; + border-bottom: 1px dashed rgba(255,255,255,0.04); + font-size: 12px; +} +.lb-row:last-child { border-bottom: none; } +.lb-row.lb-row-self { + background: linear-gradient(90deg, rgba(201,162,39,0.08), transparent); + padding-left: 6px; + border-left: 2px solid var(--warning, #c9a227); +} + +.lb-rank { + font-family: var(--font-mono); + font-weight: 700; + color: var(--status-green, #00cc33); + font-size: 11px; + letter-spacing: 1px; +} +.lb-rank-1 { color: #ffd700; text-shadow: 0 0 8px rgba(255,215,0,0.4); } +.lb-rank-2 { color: #c0c0c0; } +.lb-rank-3 { color: #cd7f32; } + +.lb-label { + color: var(--text-primary, #e0e0e0); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; +} +.lb-flag { margin-right: 6px; } +.lb-sub { + color: var(--text-secondary, #888); + font-size: 10px; + letter-spacing: 0.5px; + margin-left: 6px; +} + +.lb-count { + color: var(--accent-solana, #14F195); + font-weight: 600; + font-variant-numeric: tabular-nums; + font-size: 12px; +} + +.lb-bar-wrap { + grid-column: 1 / -1; + height: 3px; + background: rgba(255,255,255,0.04); + margin-top: 4px; + overflow: hidden; + border-radius: 1px; +} +.lb-bar { + height: 100%; + background: linear-gradient(90deg, var(--status-green, #00cc33), var(--accent-solana, #14F195)); + transform-origin: left center; + animation: lb-grow 0.7s cubic-bezier(0.22, 1, 0.36, 1) forwards; + width: 0%; + box-shadow: 0 0 8px rgba(0, 204, 51, 0.35); +} +@keyframes lb-grow { + from { width: 0%; } +} + +/* ─── Peak hours chart ─── */ +.lb-hours { + display: grid; + grid-template-columns: repeat(24, 1fr); + gap: 2px; + align-items: end; + height: 180px; + padding-bottom: 22px; + position: relative; +} +.lb-hour-col { + background: var(--status-green, #00cc33); + min-height: 2px; + border-radius: 1px 1px 0 0; + position: relative; + opacity: 0.75; + transition: opacity 0.15s, transform 0.15s; + box-shadow: 0 0 6px rgba(0, 204, 51, 0.25); +} +.lb-hour-col:hover { + opacity: 1; + transform: scaleY(1.03); + background: var(--accent-solana, #14F195); +} +.lb-hour-col::after { + content: attr(data-hour); + position: absolute; + bottom: -18px; + left: 50%; + transform: translateX(-50%); + font-size: 9px; + color: var(--text-secondary, #888); + font-family: var(--font-mono); +} +.lb-hour-col[data-showlabel="0"]::after { display: none; } +.lb-hour-col[data-peak="1"] { + background: var(--warning, #c9a227); + box-shadow: 0 0 12px rgba(201, 162, 39, 0.6); + opacity: 1; +} + +/* ─── Footer ─── */ +.lb-footer { + margin-top: 36px; + padding: 18px 0; + border-top: 1px solid var(--border-color, #2a2a2a); + font-family: var(--font-mono); + font-size: 10px; + color: var(--text-secondary, #777); + letter-spacing: 1px; + text-align: center; +} +.lb-footer code { + color: var(--status-green, #00cc33); + background: rgba(0, 204, 51, 0.08); + padding: 1px 6px; + border-radius: 2px; +} +.lb-footer p { margin: 3px 0; } + +/* ─── Mobile ─── */ +@media (max-width: 768px) { + .lb-grid { grid-template-columns: 1fr; gap: 14px; } + .lb-panel-body { max-height: 340px; min-height: 180px; } + .lb-hours { height: 140px; } + .lb-meta { font-size: 10px; gap: 8px; } + .lb-refresh-btn { margin-left: 0; } +} +@media (max-width: 420px) { + .lb-panel-header h2 { font-size: 11px; letter-spacing: 1px; } + .lb-panel-body { padding: 10px; } + .lb-row { grid-template-columns: 28px 1fr auto; font-size: 11px; } + .lb-label { font-size: 11px; } + .lb-hours { height: 110px; gap: 1px; } + .lb-hour-col::after { font-size: 8px; } +} diff --git a/hq/leaderboards.html b/hq/leaderboards.html new file mode 100644 index 0000000..d008ebe --- /dev/null +++ b/hq/leaderboards.html @@ -0,0 +1,116 @@ + + + + + + LEADERBOARDS // JAESWIFT.XYZ // HQ + + + + + + + + + + + +
+ +
+ + + + +
+
+ + +
+
+ BASE / HQ / LEADERBOARDS +
+

LEADERBOARDS

+

// LIVE OPERATIONAL RANKINGS β€” DERIVED FROM NGINX ACCESS LOGS

+
+ 24h REQUESTS: β€” + 7d REQUESTS: β€” + ● LAST UPDATED β€” + +
+
+ + +
+ +
+
+ 🌍 +

TOP COUNTRIES

+ 24h +
+
acquiring targets…
+
+ +
+
+ πŸ“„ +

TOP PAGES

+ 24h +
+
scanning routes…
+
+ +
+
+ πŸ”— +

TOP REFERRERS

+ 7d +
+
tracing inbound…
+
+ +
+
+ ⏱ +

PEAK HOURS

+ 24h UTC +
+
charting activity…
+
+ +
+
+ πŸ–₯ +

BROWSER BREAKDOWN

+ 24h +
+
fingerprinting clients…
+
+ +
+
+ πŸ‘€ +

OPERATOR LEADERBOARD

+ 7d +
+
ranking operators…
+
+ +
+ +
+

// DATA SOURCE: /var/log/nginx/access.log Β· GeoIP: GeoLite2-Country.mmdb Β· CACHE: 60s

+

// AUTO-REFRESH: 60s Β· All IPs masked to first/last octet for operator privacy

+
+ +
+
+ + + + + + + + diff --git a/js/leaderboards.js b/js/leaderboards.js new file mode 100644 index 0000000..f6449b0 --- /dev/null +++ b/js/leaderboards.js @@ -0,0 +1,283 @@ +/* =================================================== + 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); + }); + }); + }); +})();