(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); 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; var paths = svg.querySelectorAll('path[id]'); paths.forEach(function(path) { var id = path.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 path.style.fill = 'rgb(' + r + ',' + g + ',' + b + ')'; } path.addEventListener('mouseenter', function(e) { var cc = id.toUpperCase(); var n = lookup[id] || 0; tooltip.textContent = cc + ': ' + n.toLocaleString() + ' attempts'; tooltip.style.display = 'block'; }); path.addEventListener('mousemove', function(e) { tooltip.style.left = (e.clientX + 12) + 'px'; tooltip.style.top = (e.clientY - 10) + 'px'; }); path.addEventListener('mouseleave', function() { tooltip.style.display = 'none'; }); path.addEventListener('click', function() { var input = document.querySelector('#filter-form input[name="country"]'); if (input) { input.value = id.toUpperCase(); applyFilters(); } }); path.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 target = document.getElementById('dashboard-content'); if (target) { var url = '/fragments/dashboard-content?' + params.toString(); htmx.ajax('GET', url, {target: '#dashboard-content', swap: 'innerHTML'}); } // Client-side filter for recent sessions table filterSessionsTable(form); } function filterSessionsTable(form) { var ip = form.elements['ip'].value.toLowerCase(); var country = form.elements['country'].value.toLowerCase(); var username = form.elements['username'].value.toLowerCase(); var rows = document.querySelectorAll('#recent-sessions-table tbody tr'); rows.forEach(function(row) { var cells = row.querySelectorAll('td'); if (cells.length < 4) { row.style.display = ''; return; } var show = true; if (ip && cells[1].textContent.toLowerCase().indexOf(ip) === -1) show = false; if (country && cells[2].textContent.toLowerCase().indexOf(country) === -1) show = false; if (username && cells[3].textContent.toLowerCase().indexOf(username) === -1) show = false; row.style.display = show ? '' : 'none'; }); } 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(); }); } }); })();