add map to dashboard
This commit is contained in:
parent
51f99b75d4
commit
44ec9fc0a0
33
analytics.py
33
analytics.py
@ -572,6 +572,24 @@ def dashboard():
|
||||
cursor = log_db.execute(query, params_for_filter)
|
||||
locations = cursor.fetchall()
|
||||
|
||||
# Map data grouped by coordinates
|
||||
map_rows = []
|
||||
try:
|
||||
query = f'''
|
||||
SELECT city, country, latitude, longitude, COUNT(*) as count
|
||||
FROM file_access_log
|
||||
WHERE timestamp >= ? {filetype_filter_sql}
|
||||
AND latitude IS NOT NULL AND longitude IS NOT NULL
|
||||
GROUP BY city, country, latitude, longitude
|
||||
ORDER BY count DESC
|
||||
'''
|
||||
with log_db:
|
||||
cursor = log_db.execute(query, params_for_filter)
|
||||
map_rows = cursor.fetchall()
|
||||
except sqlite3.OperationalError as exc:
|
||||
# Keep dashboard working even if older DBs lack location columns
|
||||
print(f"[dashboard] map query skipped: {exc}")
|
||||
|
||||
# 7. Summary stats
|
||||
# total_accesses
|
||||
query = f'''
|
||||
@ -629,6 +647,20 @@ def dashboard():
|
||||
location_data.sort(key=lambda x: x['count'], reverse=True)
|
||||
location_data = location_data[:20]
|
||||
|
||||
# Prepare map data (limit to keep map readable)
|
||||
map_data = []
|
||||
for city, country, lat, lon, cnt in map_rows:
|
||||
if lat is None or lon is None:
|
||||
continue
|
||||
map_data.append({
|
||||
'city': city,
|
||||
'country': country,
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'count': cnt
|
||||
})
|
||||
map_data = map_data[:200]
|
||||
|
||||
title_short = app_config.get('TITLE_SHORT', 'Default Title')
|
||||
title_long = app_config.get('TITLE_LONG' , 'Default Title')
|
||||
|
||||
@ -644,6 +676,7 @@ def dashboard():
|
||||
unique_user=unique_user,
|
||||
cached_percentage=cached_percentage,
|
||||
timeframe_data=timeframe_data,
|
||||
map_data=map_data,
|
||||
admin_enabled=auth.is_admin(),
|
||||
title_short=title_short,
|
||||
title_long=title_long
|
||||
|
||||
@ -4,6 +4,20 @@
|
||||
{# 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);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{# page content #}
|
||||
{% block content %}
|
||||
|
||||
@ -185,35 +199,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IP Address Access Data -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
Verteilung der Zugriffe
|
||||
<!-- Map + Location Distribution -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-7 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Zugriffe auf der Karte</h5>
|
||||
<div id="dashboard-map"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<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 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>
|
||||
@ -225,6 +251,65 @@
|
||||
{% 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 {
|
||||
(function initDashboardMap() {
|
||||
const mapElement = document.getElementById('dashboard-map');
|
||||
if (!mapElement || typeof L === 'undefined') return;
|
||||
|
||||
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.isEmpty()) {
|
||||
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);
|
||||
})();
|
||||
} 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 }};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user