bethaus-app/templates/dashboard.html
2025-12-20 10:23:50 +00:00

479 lines
17 KiB
HTML

{# templates/dashboard.html #}
{% extends 'base.html' %}
{# page title #}
{% block title %}Dashboard{% endblock %}
{% block head_extra %}
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
#dashboard-map {
width: 100%;
height: 420px;
min-height: 320px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
#dashboard-map-modal {
width: 100%;
height: 100%;
min-height: 480px;
}
</style>
{% endblock %}
{# page content #}
{% block content %}
<!-- Main Container -->
<div class="container">
<h2>Auswertung-Downloads</h2>
<!-- Dropdown Controls -->
<div class="mb-4 d-flex flex-wrap gap-2">
<!-- Timeframe Dropdown -->
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle"
type="button" id="timeframeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
{% if session['timeframe'] == 'last24hours' %}
Last 24 Hours
{% elif session['timeframe'] == '7days' %}
Last 7 Days
{% elif session['timeframe'] == '14days' %}
Last 14 Days
{% elif session['timeframe'] == '30days' %}
Last 30 Days
{% elif session['timeframe'] == '365days' %}
Last 365 Days
{% else %}
Select Timeframe
{% endif %}
</button>
<ul class="dropdown-menu" aria-labelledby="timeframeDropdown">
<li>
<a class="dropdown-item {% if session['timeframe'] == 'last24hours' %}active{% endif %}"
href="{{ url_for('dashboard', timeframe='last24hours') }}">
Last 24 Hours
</a>
</li>
<li>
<a class="dropdown-item {% if session['timeframe'] == '7days' %}active{% endif %}"
href="{{ url_for('dashboard', timeframe='7days') }}">
Last 7 Days
</a>
</li>
<li>
<a class="dropdown-item {% if session['timeframe'] == '14days' %}active{% endif %}"
href="{{ url_for('dashboard', timeframe='14days') }}">
Last 14 Days
</a>
</li>
<li>
<a class="dropdown-item {% if session['timeframe'] == '30days' %}active{% endif %}"
href="{{ url_for('dashboard', timeframe='30days') }}">
Last 30 Days
</a>
</li>
<li>
<a class="dropdown-item {% if session['timeframe'] == '365days' %}active{% endif %}"
href="{{ url_for('dashboard', timeframe='365days') }}">
Last 365 Days
</a>
</li>
</ul>
</div>
<!-- Filetype Dropdown -->
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle"
type="button" id="filetypeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
{% if session['filetype'] == 'audio' %}
Audio
{% elif session['filetype'] == 'video' %}
Video
{% elif session['filetype'] == 'photo' %}
Photo
{% elif session['filetype'] == 'other' %}
Other
{% else %}
Select File Type
{% endif %}
</button>
<ul class="dropdown-menu" aria-labelledby="filetypeDropdown">
<li>
<a class="dropdown-item {% if session['filetype'] == 'audio' %}active{% endif %}"
href="{{ url_for('dashboard', filetype='audio') }}">
Audio
</a>
</li>
<li>
<a class="dropdown-item {% if session['filetype'] == 'video' %}active{% endif %}"
href="{{ url_for('dashboard', filetype='video') }}">
Video
</a>
</li>
<li>
<a class="dropdown-item {% if session['filetype'] == 'photo' %}active{% endif %}"
href="{{ url_for('dashboard', filetype='photo') }}">
Photo
</a>
</li>
<li>
<a class="dropdown-item {% if session['filetype'] == 'other' %}active{% endif %}"
href="{{ url_for('dashboard', filetype='other') }}">
Other
</a>
</li>
</ul>
</div>
</div>
<!-- Summary Cards -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card bg-info text-white">
<div class="card-body">
<h5 class="card-title">Alle Downloads</h5>
<p class="card-text display-6">{{ total_accesses }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-success text-white">
<div class="card-body">
<h5 class="card-title">unterschiedliche Dateien</h5>
<p class="card-text display-6">{{ unique_files }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-warning text-white">
<div class="card-body">
<h5 class="card-title">unterschiedliche Nutzer</h5>
<p class="card-text display-6">{{ unique_user }}</p>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card bg-secondary text-white">
<div class="card-body">
<h5 class="card-title">beschleunigte Downloads</h5>
<p class="card-text display-6">{{ cached_percentage }} %</p>
</div>
</div>
</div>
</div>
<!-- Charts Section -->
<div class="row mb-4">
<!-- Access Trend Chart -->
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Anzahl Geräte</h5>
<canvas id="distinctDeviceChart"></canvas>
</div>
</div>
</div>
<!-- Timeframe Breakdown Chart -->
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Anzahl Downloads</h5>
<canvas id="downloadTimeframeChart"></canvas>
</div>
</div>
</div>
<!-- User Agent Distribution Chart -->
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Endgeräte der Nutzer</h5>
<canvas id="userAgentChart"></canvas>
</div>
</div>
</div>
<!-- Folder Distribution Chart -->
<div class="col-md-6 mb-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Verteilung auf Ordner</h5>
<canvas id="folderChart"></canvas>
</div>
</div>
</div>
</div>
<!-- Map + Location Distribution -->
<div class="row mb-4">
<div class="col-lg-7 mb-3">
<div class="card h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Zugriffe auf der Karte</h5>
<button id="mapFullscreenToggle" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#mapModal">Vollbild</button>
</div>
<div id="dashboard-map" class="mt-3"></div>
</div>
</div>
</div>
<div class="col-lg-5 mb-3">
<div class="card h-100">
<div class="card-header">
Verteilung der Zugriffe
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered mb-0">
<thead>
<tr>
<th>Anzahl Downloads</th>
<th>Stadt</th>
<th>Land</th>
</tr>
</thead>
<tbody>
{% for loc in location_data %}
<tr>
<td>{{ loc.count }}</td>
<td>{{ loc.city }}</td>
<td>{{ loc.country }}</td>
</tr>
{% else %}
<tr>
<td colspan="3">No access data available for the selected timeframe.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Map fullscreen modal -->
<div class="modal fade" id="mapModal" tabindex="-1" aria-labelledby="mapModalLabel" aria-hidden="true">
<div class="modal-dialog modal-fullscreen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mapModalLabel">Karte im Vollbild</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<div id="dashboard-map-modal"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const rawMapData = {{ map_data|tojson|default('[]') }};
const mapData = Array.isArray(rawMapData) ? rawMapData : [];
// Leaflet map with aggregated downloads (fail-safe so charts still render)
try {
const createLeafletMap = (mapElement) => {
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);
return { map, refreshSize };
};
(function initDashboardMap() {
const mapElement = document.getElementById('dashboard-map');
if (!mapElement || typeof L === 'undefined') return;
createLeafletMap(mapElement);
const modalEl = document.getElementById('mapModal');
let modalMapInstance = null;
if (modalEl) {
modalEl.addEventListener('shown.bs.modal', () => {
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 (!modalMapInstance) {
const { map, refreshSize } = createLeafletMap(modalMapElement);
modalMapInstance = { map, refreshSize };
}
// Ensure proper sizing after modal animation completes
setTimeout(() => {
modalMapInstance.map.invalidateSize();
if (modalMapInstance.refreshSize) modalMapInstance.refreshSize();
}, 200);
});
}
})();
} catch (err) {
console.error('Dashboard map init failed:', err);
}
// Data passed from the backend as JSON
let distinctDeviceData = {{ distinct_device_data|tojson }};
let timeframeData = {{ timeframe_data|tojson }};
const userAgentData = {{ user_agent_data|tojson }};
const folderData = {{ folder_data|tojson }};
// Remove the first (incomplete) bucket from the arrays
// distinctDeviceData = distinctDeviceData.slice(1);
// timeframeData = timeframeData.slice(1);
// Shift the labels to local time zone
const timeframe = "{{ timeframe }}"; // e.g., 'last24hours', '7days', '30days', or '365days'
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;
}
});
// Distinct Device Chart
const ctxDistinctDevice = document.getElementById('distinctDeviceChart').getContext('2d');
new Chart(ctxDistinctDevice, {
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: { stepSize: 1 }
}
}
}
});
// Timeframe Breakdown Chart
const ctxTimeframe = document.getElementById('downloadTimeframeChart').getContext('2d');
new Chart(ctxTimeframe, {
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: { stepSize: 1 }
}
}
}
});
// User Agent Distribution Chart - Pie Chart
const ctxUserAgent = document.getElementById('userAgentChart').getContext('2d');
new Chart(ctxUserAgent, {
type: 'pie',
data: {
labels: userAgentData.map(item => item.device),
datasets: [{
data: userAgentData.map(item => item.count)
}]
},
options: { responsive: true }
});
// Folder Distribution Chart - Pie Chart
const ctxFolder = document.getElementById('folderChart').getContext('2d');
new Chart(ctxFolder, {
type: 'pie',
data: {
labels: folderData.map(item => item.folder),
datasets: [{
data: folderData.map(item => item.count)
}]
},
options: { responsive: true }
});
</script>
{% endblock %}