264 lines
8.4 KiB
JavaScript
264 lines
8.4 KiB
JavaScript
(() => {
|
|
let charts = [];
|
|
let mainMap = null;
|
|
let modalMap = null;
|
|
let refreshHandlers = [];
|
|
let modalShownHandler = null;
|
|
|
|
function parseDashboardData() {
|
|
const script = document.getElementById('dashboard-page-data');
|
|
if (!script) return null;
|
|
try {
|
|
return JSON.parse(script.textContent || '{}');
|
|
} catch (err) {
|
|
console.error('Failed to parse dashboard data', err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function createLeafletMap(mapElement, mapData) {
|
|
const map = L.map(mapElement).setView([50, 10], 4);
|
|
const bounds = L.latLngBounds();
|
|
const defaultCenter = [50, 10];
|
|
const defaultZoom = 4;
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors',
|
|
maxZoom: 19
|
|
}).addTo(map);
|
|
|
|
mapData.forEach(point => {
|
|
if (point.lat === null || point.lon === null) return;
|
|
const radius = Math.max(6, 4 + Math.log(point.count + 1) * 4);
|
|
const marker = L.circleMarker([point.lat, point.lon], {
|
|
radius,
|
|
color: '#ff0000',
|
|
fillColor: '#ff4444',
|
|
fillOpacity: 0.75,
|
|
weight: 2
|
|
}).addTo(map);
|
|
|
|
marker.bindPopup(`
|
|
<strong>${point.city}, ${point.country}</strong><br>
|
|
Downloads: ${point.count}
|
|
`);
|
|
bounds.extend([point.lat, point.lon]);
|
|
});
|
|
|
|
if (mapData.length === 0) {
|
|
const msg = document.createElement('div');
|
|
msg.textContent = 'Keine Geo-Daten für den ausgewählten Zeitraum.';
|
|
msg.className = 'text-muted small mt-3';
|
|
mapElement.appendChild(msg);
|
|
}
|
|
|
|
if (bounds.isValid()) {
|
|
map.fitBounds(bounds, { padding: [24, 24] });
|
|
} else {
|
|
map.setView(defaultCenter, defaultZoom);
|
|
}
|
|
|
|
const refreshSize = () => setTimeout(() => map.invalidateSize(), 150);
|
|
refreshSize();
|
|
window.addEventListener('resize', refreshSize);
|
|
window.addEventListener('orientationchange', refreshSize);
|
|
refreshHandlers.push(refreshSize);
|
|
return { map, refreshSize };
|
|
}
|
|
|
|
function initDashboardPage() {
|
|
const mapElement = document.getElementById('dashboard-map');
|
|
if (!mapElement || mapElement.dataset.bound) return;
|
|
mapElement.dataset.bound = '1';
|
|
|
|
const data = parseDashboardData();
|
|
if (!data) return;
|
|
|
|
const mapData = Array.isArray(data.map_data) ? data.map_data : [];
|
|
const distinctDeviceData = Array.isArray(data.distinct_device_data) ? data.distinct_device_data : [];
|
|
const timeframeData = Array.isArray(data.timeframe_data) ? data.timeframe_data : [];
|
|
const userAgentData = Array.isArray(data.user_agent_data) ? data.user_agent_data : [];
|
|
const folderData = Array.isArray(data.folder_data) ? data.folder_data : [];
|
|
const timeframe = data.timeframe || '';
|
|
|
|
if (typeof L !== 'undefined') {
|
|
try {
|
|
mainMap = createLeafletMap(mapElement, mapData);
|
|
|
|
const modalEl = document.getElementById('mapModal');
|
|
if (modalEl) {
|
|
modalShownHandler = () => {
|
|
const modalMapElement = document.getElementById('dashboard-map-modal');
|
|
if (!modalMapElement) return;
|
|
const modalBody = modalEl.querySelector('.modal-body');
|
|
const modalHeader = modalEl.querySelector('.modal-header');
|
|
if (modalBody && modalHeader) {
|
|
const availableHeight = window.innerHeight - modalHeader.offsetHeight;
|
|
modalBody.style.height = `${availableHeight}px`;
|
|
modalMapElement.style.height = '100%';
|
|
}
|
|
if (!modalMap) {
|
|
modalMap = createLeafletMap(modalMapElement, mapData);
|
|
}
|
|
setTimeout(() => {
|
|
if (modalMap && modalMap.map) {
|
|
modalMap.map.invalidateSize();
|
|
if (modalMap.refreshSize) modalMap.refreshSize();
|
|
}
|
|
}, 200);
|
|
};
|
|
modalEl.addEventListener('shown.bs.modal', modalShownHandler);
|
|
}
|
|
} catch (err) {
|
|
console.error('Dashboard map init failed:', err);
|
|
}
|
|
}
|
|
|
|
if (typeof Chart === 'undefined') return;
|
|
|
|
const shiftedLabels = timeframeData.map((item) => {
|
|
if (timeframe === 'last24hours') {
|
|
const bucketDate = new Date(item.bucket);
|
|
const now = new Date();
|
|
const isCurrentHour =
|
|
bucketDate.getFullYear() === now.getFullYear() &&
|
|
bucketDate.getMonth() === now.getMonth() &&
|
|
bucketDate.getDate() === now.getDate() &&
|
|
bucketDate.getHours() === now.getHours();
|
|
const bucketEnd = isCurrentHour ? now : new Date(bucketDate.getTime() + 3600 * 1000);
|
|
return `${bucketDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - ${bucketEnd.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
|
} else if (timeframe === '7days' || timeframe === '30days') {
|
|
const localDate = new Date(item.bucket);
|
|
return localDate.toLocaleDateString();
|
|
} else if (timeframe === '365days') {
|
|
const [year, month] = item.bucket.split('-');
|
|
const dateObj = new Date(year, month - 1, 1);
|
|
return dateObj.toLocaleString([], { month: 'short', year: 'numeric' });
|
|
} else {
|
|
return item.bucket;
|
|
}
|
|
});
|
|
|
|
const ctxDistinctDevice = document.getElementById('distinctDeviceChart');
|
|
if (ctxDistinctDevice) {
|
|
charts.push(new Chart(ctxDistinctDevice.getContext('2d'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: shiftedLabels,
|
|
datasets: [{
|
|
label: 'Device Count',
|
|
data: distinctDeviceData.map(item => item.count),
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
x: { title: { display: true, text: 'Time Range' } },
|
|
y: {
|
|
title: { display: true, text: 'Device Count' },
|
|
beginAtZero: true,
|
|
ticks: { maxTicksLimit: 10 }
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
const ctxTimeframe = document.getElementById('downloadTimeframeChart');
|
|
if (ctxTimeframe) {
|
|
charts.push(new Chart(ctxTimeframe.getContext('2d'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: shiftedLabels,
|
|
datasets: [{
|
|
label: 'Download Count',
|
|
data: timeframeData.map(item => item.count),
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
x: { title: { display: true, text: 'Time Range' } },
|
|
y: {
|
|
title: { display: true, text: 'Download Count' },
|
|
beginAtZero: true,
|
|
ticks: { maxTicksLimit: 10 }
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
const ctxUserAgent = document.getElementById('userAgentChart');
|
|
if (ctxUserAgent) {
|
|
charts.push(new Chart(ctxUserAgent.getContext('2d'), {
|
|
type: 'pie',
|
|
data: {
|
|
labels: userAgentData.map(item => item.device),
|
|
datasets: [{
|
|
data: userAgentData.map(item => item.count)
|
|
}]
|
|
},
|
|
options: { responsive: true }
|
|
}));
|
|
}
|
|
|
|
const ctxFolder = document.getElementById('folderChart');
|
|
if (ctxFolder) {
|
|
charts.push(new Chart(ctxFolder.getContext('2d'), {
|
|
type: 'pie',
|
|
data: {
|
|
labels: folderData.map(item => item.folder),
|
|
datasets: [{
|
|
data: folderData.map(item => item.count)
|
|
}]
|
|
},
|
|
options: { responsive: true }
|
|
}));
|
|
}
|
|
}
|
|
|
|
function destroyDashboardPage() {
|
|
charts.forEach(chart => {
|
|
try {
|
|
chart.destroy();
|
|
} catch (err) {
|
|
// ignore
|
|
}
|
|
});
|
|
charts = [];
|
|
|
|
if (mainMap && mainMap.map) {
|
|
mainMap.map.remove();
|
|
}
|
|
if (modalMap && modalMap.map) {
|
|
modalMap.map.remove();
|
|
}
|
|
mainMap = null;
|
|
modalMap = null;
|
|
|
|
refreshHandlers.forEach(handler => {
|
|
window.removeEventListener('resize', handler);
|
|
window.removeEventListener('orientationchange', handler);
|
|
});
|
|
refreshHandlers = [];
|
|
|
|
const modalEl = document.getElementById('mapModal');
|
|
if (modalEl && modalShownHandler) {
|
|
modalEl.removeEventListener('shown.bs.modal', modalShownHandler);
|
|
}
|
|
modalShownHandler = null;
|
|
}
|
|
|
|
if (window.PageRegistry && typeof window.PageRegistry.register === 'function') {
|
|
window.PageRegistry.register('dashboard', {
|
|
init: initDashboardPage,
|
|
destroy: destroyDashboardPage
|
|
});
|
|
}
|
|
})();
|