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)
|
cursor = log_db.execute(query, params_for_filter)
|
||||||
locations = cursor.fetchall()
|
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
|
# 7. Summary stats
|
||||||
# total_accesses
|
# total_accesses
|
||||||
query = f'''
|
query = f'''
|
||||||
@ -629,6 +647,20 @@ def dashboard():
|
|||||||
location_data.sort(key=lambda x: x['count'], reverse=True)
|
location_data.sort(key=lambda x: x['count'], reverse=True)
|
||||||
location_data = location_data[:20]
|
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_short = app_config.get('TITLE_SHORT', 'Default Title')
|
||||||
title_long = app_config.get('TITLE_LONG' , 'Default Title')
|
title_long = app_config.get('TITLE_LONG' , 'Default Title')
|
||||||
|
|
||||||
@ -644,6 +676,7 @@ def dashboard():
|
|||||||
unique_user=unique_user,
|
unique_user=unique_user,
|
||||||
cached_percentage=cached_percentage,
|
cached_percentage=cached_percentage,
|
||||||
timeframe_data=timeframe_data,
|
timeframe_data=timeframe_data,
|
||||||
|
map_data=map_data,
|
||||||
admin_enabled=auth.is_admin(),
|
admin_enabled=auth.is_admin(),
|
||||||
title_short=title_short,
|
title_short=title_short,
|
||||||
title_long=title_long
|
title_long=title_long
|
||||||
|
|||||||
@ -4,6 +4,20 @@
|
|||||||
{# page title #}
|
{# page title #}
|
||||||
{% block title %}Dashboard{% endblock %}
|
{% 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 #}
|
{# page content #}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
@ -185,14 +199,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- IP Address Access Data -->
|
<!-- Map + Location Distribution -->
|
||||||
<div class="card mb-4">
|
<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="col-lg-5 mb-3">
|
||||||
|
<div class="card h-100">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Verteilung der Zugriffe
|
Verteilung der Zugriffe
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Anzahl Downloads</th>
|
<th>Anzahl Downloads</th>
|
||||||
@ -217,6 +241,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -225,6 +251,65 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<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
|
// Data passed from the backend as JSON
|
||||||
let distinctDeviceData = {{ distinct_device_data|tojson }};
|
let distinctDeviceData = {{ distinct_device_data|tojson }};
|
||||||
let timeframeData = {{ timeframe_data|tojson }};
|
let timeframeData = {{ timeframe_data|tojson }};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user