(function() { 'use strict'; // Chart.js theme for Pico dark mode Chart.defaults.color = '#b0b0b8'; Chart.defaults.borderColor = '#3a3a4a'; var attemptsChart = null; var hourlyChart = null; function getFilterParams() { var form = document.getElementById('filter-form'); if (!form) return ''; var params = new URLSearchParams(); var since = form.elements['since'].value; var until = form.elements['until'].value; if (since) params.set('since', since); if (until) params.set('until', until); var humanScore = form.elements['human_score']; if (humanScore && humanScore.checked) params.set('human_score', '1'); var sortBy = form.elements['sort']; if (sortBy && sortBy.value) params.set('sort', sortBy.value); return params.toString(); } function initAttemptsChart() { var canvas = document.getElementById('chart-attempts'); if (!canvas) return; var ctx = canvas.getContext('2d'); var qs = getFilterParams(); var url = '/api/charts/attempts-over-time' + (qs ? '?' + qs : ''); fetch(url) .then(function(r) { return r.json(); }) .then(function(data) { var labels = data.points.map(function(p) { return p.date; }); var values = data.points.map(function(p) { return p.count; }); if (attemptsChart) { attemptsChart.data.labels = labels; attemptsChart.data.datasets[0].data = values; attemptsChart.update(); return; } attemptsChart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [{ label: 'Attempts', data: values, borderColor: '#6366f1', backgroundColor: 'rgba(99, 102, 241, 0.1)', fill: true, tension: 0.3, pointRadius: 2 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } }, y: { beginAtZero: true } } } }); }); } function initHourlyChart() { var canvas = document.getElementById('chart-hourly'); if (!canvas) return; var ctx = canvas.getContext('2d'); var qs = getFilterParams(); var url = '/api/charts/hourly-pattern' + (qs ? '?' + qs : ''); fetch(url) .then(function(r) { return r.json(); }) .then(function(data) { // Fill all 24 hours, defaulting to 0 var hourMap = {}; data.hours.forEach(function(h) { hourMap[h.hour] = h.count; }); var labels = []; var values = []; for (var i = 0; i < 24; i++) { labels.push(i + ':00'); values.push(hourMap[i] || 0); } if (hourlyChart) { hourlyChart.data.labels = labels; hourlyChart.data.datasets[0].data = values; hourlyChart.update(); return; } hourlyChart = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [{ label: 'Attempts', data: values, backgroundColor: 'rgba(99, 102, 241, 0.6)', borderColor: '#6366f1', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { grid: { display: false } }, y: { beginAtZero: true } } } }); }); } function initWorldMap() { var container = document.getElementById('world-map'); if (!container) return; fetch('/static/world.svg') .then(function(r) { return r.text(); }) .then(function(svgText) { container.innerHTML = svgText; fetch('/api/charts/country-stats') .then(function(r) { return r.json(); }) .then(function(data) { colorMap(container, data.countries); }); }); } function colorMap(container, countries) { if (!countries || countries.length === 0) return; var maxCount = countries[0].count; // already sorted DESC var logMax = Math.log(maxCount + 1); // Build lookup var lookup = {}; countries.forEach(function(c) { lookup[c.country.toLowerCase()] = c.count; }); // Create tooltip element var tooltip = document.createElement('div'); tooltip.id = 'map-tooltip'; tooltip.style.cssText = 'position:fixed;display:none;background:#1a1a2e;color:#e0e0e8;padding:4px 8px;border-radius:4px;font-size:13px;pointer-events:none;z-index:1000;border:1px solid #3a3a4a;'; document.body.appendChild(tooltip); var svg = container.querySelector('svg'); if (!svg) return; // Remove SVG title to prevent browser native tooltip var svgTitle = svg.querySelector('title'); if (svgTitle) svgTitle.remove(); // Select both and country elements var elements = svg.querySelectorAll('path[id], g[id]'); elements.forEach(function(el) { var id = el.id.toLowerCase(); if (id.charAt(0) === '_') return; // skip non-country paths var count = lookup[id]; if (count) { var intensity = Math.log(count + 1) / logMax; var r = Math.round(30 + intensity * 69); // 30 -> 99 var g = Math.round(30 + intensity * 72); // 30 -> 102 var b = Math.round(62 + intensity * 179); // 62 -> 241 var color = 'rgb(' + r + ',' + g + ',' + b + ')'; // For elements, color child paths; for , color directly if (el.tagName.toLowerCase() === 'g') { el.querySelectorAll('path').forEach(function(p) { p.style.fill = color; }); } else { el.style.fill = color; } } el.addEventListener('mouseenter', function(e) { var cc = id.toUpperCase(); var n = lookup[id] || 0; tooltip.textContent = cc + ': ' + n.toLocaleString() + ' attempts'; tooltip.style.display = 'block'; }); el.addEventListener('mousemove', function(e) { tooltip.style.left = (e.clientX + 12) + 'px'; tooltip.style.top = (e.clientY - 10) + 'px'; }); el.addEventListener('mouseleave', function() { tooltip.style.display = 'none'; }); el.addEventListener('click', function() { var input = document.querySelector('#filter-form input[name="country"]'); if (input) { input.value = id.toUpperCase(); applyFilters(); } }); el.style.cursor = 'pointer'; }); } function applyFilters() { // Re-fetch charts with filter params initAttemptsChart(); initHourlyChart(); // Re-fetch dashboard content via htmx var form = document.getElementById('filter-form'); if (!form) return; var params = new URLSearchParams(); ['since', 'until', 'ip', 'country', 'username'].forEach(function(name) { var val = form.elements[name].value; if (val) params.set(name, val); }); var humanScore = form.elements['human_score']; if (humanScore && humanScore.checked) params.set('human_score', '1'); var sortBy = form.elements['sort']; if (sortBy && sortBy.value) params.set('sort', sortBy.value); var target = document.getElementById('dashboard-content'); if (target) { var url = '/fragments/dashboard-content?' + params.toString(); htmx.ajax('GET', url, {target: '#dashboard-content', swap: 'innerHTML'}); } // Server-side filter for recent sessions table var sessionsUrl = '/fragments/recent-sessions?' + params.toString(); htmx.ajax('GET', sessionsUrl, {target: '#recent-sessions-table tbody', swap: 'innerHTML'}); } window.clearFilters = function() { var form = document.getElementById('filter-form'); if (form) { form.reset(); applyFilters(); } }; window.applyFilters = applyFilters; // Initialize on DOM ready document.addEventListener('DOMContentLoaded', function() { initAttemptsChart(); initHourlyChart(); initWorldMap(); var form = document.getElementById('filter-form'); if (form) { form.addEventListener('submit', function(e) { e.preventDefault(); applyFilters(); }); } }); })();