+81
-5
src/dashboard.html
+81
-5
src/dashboard.html
···
358
358
<script>
359
359
// State
360
360
let currentDays = 7;
361
+
let currentZoom = null; // { start, end } when zoomed
361
362
let chart = null;
363
+
let dblClickHandler = null;
362
364
let currentTrafficData = [];
363
365
let allUserAgents = [];
364
366
let abortController = null;
367
+
368
+
// URL state management
369
+
function getStateFromURL() {
370
+
const params = new URLSearchParams(window.location.search);
371
+
const days = parseInt(params.get('days'));
372
+
const start = parseInt(params.get('start'));
373
+
const end = parseInt(params.get('end'));
374
+
375
+
return {
376
+
days: days && [1, 7, 30, 90, 365].includes(days) ? days : 7,
377
+
zoom: start && end ? { start, end } : null
378
+
};
379
+
}
380
+
381
+
function updateURL() {
382
+
const params = new URLSearchParams();
383
+
params.set('days', currentDays);
384
+
if (currentZoom) {
385
+
params.set('start', currentZoom.start);
386
+
params.set('end', currentZoom.end);
387
+
}
388
+
const newURL = `${window.location.pathname}?${params.toString()}`;
389
+
history.replaceState(null, '', newURL);
390
+
}
365
391
366
392
// Utilities
367
393
function formatNumber(n) {
···
467
493
{
468
494
side: 1,
469
495
scale: 'latency',
470
-
stroke: '#f97316',
496
+
stroke: 'rgba(249, 115, 22, 0.7)',
471
497
grid: { show: false },
472
498
ticks: { stroke: '#30363d', width: 1 },
473
499
font: '11px system-ui',
···
509
535
510
536
chart = new uPlot(opts, [timestamps, hits, latency], container);
511
537
512
-
// Double-click to reset zoom
513
-
container.addEventListener('dblclick', () => {
538
+
// Double-click to reset zoom (remove old handler first)
539
+
if (dblClickHandler) {
540
+
container.removeEventListener('dblclick', dblClickHandler);
541
+
}
542
+
dblClickHandler = () => {
543
+
currentZoom = null;
544
+
updateURL();
514
545
loadData();
515
-
});
546
+
};
547
+
container.addEventListener('dblclick', dblClickHandler);
516
548
}
517
549
518
550
function handleZoom(minTime, maxTime) {
···
525
557
minTime = Math.floor(center - minSpan / 2);
526
558
maxTime = Math.floor(center + minSpan / 2);
527
559
}
560
+
561
+
currentZoom = { start: Math.floor(minTime), end: Math.floor(maxTime) };
562
+
updateURL();
528
563
529
564
showLoading(true);
530
565
fetchTrafficData(minTime, maxTime).then(data => {
···
641
676
document.querySelectorAll('.time-btn').forEach(b => b.classList.remove('active'));
642
677
btn.classList.add('active');
643
678
currentDays = parseInt(btn.dataset.days);
679
+
currentZoom = null; // Reset zoom when changing time range
680
+
updateURL();
644
681
loadData();
645
682
});
646
683
});
···
671
708
});
672
709
673
710
// Initialize
674
-
document.addEventListener('DOMContentLoaded', loadData);
711
+
document.addEventListener('DOMContentLoaded', () => {
712
+
const state = getStateFromURL();
713
+
currentDays = state.days;
714
+
currentZoom = state.zoom;
715
+
716
+
// Update active button
717
+
document.querySelectorAll('.time-btn').forEach(b => {
718
+
b.classList.toggle('active', parseInt(b.dataset.days) === currentDays);
719
+
});
720
+
721
+
// Load with zoom if present
722
+
if (currentZoom) {
723
+
showLoading(true);
724
+
Promise.all([
725
+
fetch(`/stats/essential?days=${currentDays}`),
726
+
fetchTrafficData(currentZoom.start, currentZoom.end),
727
+
fetch('/stats/useragents')
728
+
]).then(async ([statsRes, trafficData, uaRes]) => {
729
+
if (trafficData === null) {
730
+
showLoading(false);
731
+
return;
732
+
}
733
+
const stats = await statsRes.json();
734
+
const userAgents = await uaRes.json();
735
+
736
+
document.getElementById('totalRequests').textContent = formatNumber(stats.totalRequests || 0);
737
+
document.getElementById('avgResponseTime').textContent = formatMs(stats.averageResponseTime);
738
+
document.getElementById('uptime').textContent = stats.uptime ? `${stats.uptime.toFixed(1)}%` : '-';
739
+
document.getElementById('uniqueAgents').textContent = formatNumber(userAgents.length || 0);
740
+
741
+
currentTrafficData = trafficData;
742
+
initChart(trafficData);
743
+
allUserAgents = userAgents;
744
+
renderUserAgents(userAgents);
745
+
showLoading(false);
746
+
});
747
+
} else {
748
+
loadData();
749
+
}
750
+
});
675
751
</script>
676
752
</body>
677
753