improve dashboard

This commit is contained in:
lelo 2025-03-18 22:38:11 +00:00
parent d233e66268
commit ab79098e9a
4 changed files with 133 additions and 52 deletions

79
app.py
View File

@ -6,18 +6,16 @@ from functools import wraps
import mimetypes import mimetypes
import sqlite3 import sqlite3
from datetime import datetime, date, timedelta from datetime import datetime, date, timedelta
from urllib.parse import unquote
import diskcache import diskcache
import json import json
import geoip2.database import geoip2.database
from urllib.parse import urlparse, unquote
from werkzeug.middleware.proxy_fix import ProxyFix from werkzeug.middleware.proxy_fix import ProxyFix
cache = diskcache.Cache('./filecache', size_limit= 32 * 1024**3) # 32 GB limit cache = diskcache.Cache('./filecache', size_limit= 48 * 1024**3) # 32 GB limit
app = Flask(__name__) app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
# Use a raw string for the default FILE_ROOT path.
app.config['FILE_ROOT'] = r'/mp3_root'
app.config['SECRET_KEY'] = '85c1117eb3a5f2c79f0ff395bada8ff8d9a257b99ef5e143' app.config['SECRET_KEY'] = '85c1117eb3a5f2c79f0ff395bada8ff8d9a257b99ef5e143'
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=90) app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=90)
app.config['SESSION_COOKIE_SAMESITE'] = 'None' app.config['SESSION_COOKIE_SAMESITE'] = 'None'
@ -131,8 +129,9 @@ def list_directory_contents(directory, subpath):
full_path = os.path.join(directory, item) full_path = os.path.join(directory, item)
# Process directories. # Process directories.
if os.path.isdir(full_path): if os.path.isdir(full_path):
# Also skip the "Transkription" folder. # skip folder
if item.lower() == "transkription": skip_folder = ["Transkription", "@eaDir"]
if item in skip_folder:
continue continue
rel_path = os.path.join(subpath, item) if subpath else item rel_path = os.path.join(subpath, item) if subpath else item
rel_path = rel_path.replace(os.sep, '/') rel_path = rel_path.replace(os.sep, '/')
@ -211,6 +210,28 @@ def lookup_location(ip, reader):
except Exception: except Exception:
return "Unknown", "Unknown" return "Unknown", "Unknown"
# Helper function to classify device type based on user agent string
def get_device_type(user_agent):
if 'Android' in user_agent:
return 'Android'
elif 'iPhone' in user_agent or 'iPad' in user_agent:
return 'iOS'
elif 'Windows' in user_agent:
return 'Windows'
elif 'Macintosh' in user_agent or 'Mac OS' in user_agent:
return 'MacOS'
elif 'Linux' in user_agent:
return 'Linux'
else:
return 'Other'
def shorten_referrer(url):
segments = [seg for seg in url.split('/') if seg]
segment = segments[-1]
# Decode all percent-encoded characters (like %20, %2F, etc.)
segment_decoded = unquote(segment)
return segment_decoded
@app.route("/dashboard") @app.route("/dashboard")
@require_secret @require_secret
def dashboard(): def dashboard():
@ -238,6 +259,7 @@ def dashboard():
WHERE timestamp >= ? WHERE timestamp >= ?
GROUP BY full_path GROUP BY full_path
ORDER BY access_count DESC ORDER BY access_count DESC
LIMIT 20
''', (start.isoformat(),)) ''', (start.isoformat(),))
rows = cursor.fetchall() rows = cursor.fetchall()
@ -262,18 +284,23 @@ def dashboard():
''', (start.isoformat(),)) ''', (start.isoformat(),))
top_files_data = [dict(full_path=row[0], access_count=row[1]) for row in cursor.fetchall()] top_files_data = [dict(full_path=row[0], access_count=row[1]) for row in cursor.fetchall()]
# User agent distribution (limit to 10) # User agent distribution (aggregate by device type)
cursor.execute(''' cursor.execute('''
SELECT user_agent, COUNT(*) as count SELECT user_agent, COUNT(*) as count
FROM file_access_log FROM file_access_log
WHERE timestamp >= ? WHERE timestamp >= ?
GROUP BY user_agent GROUP BY user_agent
ORDER BY count DESC ORDER BY count DESC
LIMIT 10
''', (start.isoformat(),)) ''', (start.isoformat(),))
user_agent_data = [dict(user_agent=row[0], count=row[1]) for row in cursor.fetchall()] raw_user_agents = [dict(user_agent=row[0], count=row[1]) for row in cursor.fetchall()]
device_counts = {}
for entry in raw_user_agents:
device = get_device_type(entry['user_agent'])
device_counts[device] = device_counts.get(device, 0) + entry['count']
# Rename to user_agent_data for compatibility with the frontend
user_agent_data = [dict(device=device, count=count) for device, count in device_counts.items()]
# Referrer distribution (limit to 10) # Referrer distribution (shorten links)
cursor.execute(''' cursor.execute('''
SELECT referrer, COUNT(*) as count SELECT referrer, COUNT(*) as count
FROM file_access_log FROM file_access_log
@ -282,7 +309,11 @@ def dashboard():
ORDER BY count DESC ORDER BY count DESC
LIMIT 10 LIMIT 10
''', (start.isoformat(),)) ''', (start.isoformat(),))
referrer_data = [dict(referrer=row[0] if row[0] else "Direct/None", count=row[1]) for row in cursor.fetchall()] referrer_data = []
for row in cursor.fetchall():
raw_ref = row[0]
shortened = shorten_referrer(raw_ref) if raw_ref else "Direct/None"
referrer_data.append(dict(referrer=shortened, count=row[1]))
# Aggregate IP addresses with counts # Aggregate IP addresses with counts
cursor.execute(''' cursor.execute('''
@ -291,6 +322,7 @@ def dashboard():
WHERE timestamp >= ? WHERE timestamp >= ?
GROUP BY ip_address GROUP BY ip_address
ORDER BY count DESC ORDER BY count DESC
LIMIT 20
''', (start.isoformat(),)) ''', (start.isoformat(),))
ip_rows = cursor.fetchall() ip_rows = cursor.fetchall()
@ -302,6 +334,13 @@ def dashboard():
ip_data.append(dict(ip=ip, count=count, country=country, city=city)) ip_data.append(dict(ip=ip, count=count, country=country, city=city))
reader.close() reader.close()
# Aggregate by city (ignoring entries without a city)
city_counts = {}
for entry in ip_data:
if entry['city']:
city_counts[entry['city']] = city_counts.get(entry['city'], 0) + entry['count']
city_data = [dict(city=city, count=count) for city, count in city_counts.items()]
# Summary stats # Summary stats
total_accesses = sum([row[1] for row in rows]) total_accesses = sum([row[1] for row in rows])
unique_files = len(rows) unique_files = len(rows)
@ -317,6 +356,7 @@ def dashboard():
user_agent_data=user_agent_data, user_agent_data=user_agent_data,
referrer_data=referrer_data, referrer_data=referrer_data,
ip_data=ip_data, ip_data=ip_data,
city_data=city_data,
total_accesses=total_accesses, total_accesses=total_accesses,
unique_files=unique_files, unique_files=unique_files,
unique_ips=unique_ips) unique_ips=unique_ips)
@ -364,18 +404,21 @@ def serve_file(filename):
app.logger.error(f"File not found: {full_path}") app.logger.error(f"File not found: {full_path}")
return "File not found", 404 return "File not found", 404
# HEAD request are coming in to initiate server caching.
# only log initial hits and not the reload of further file parts
range_header = request.headers.get('Range')
if request.method != 'HEAD' and (not range_header or range_header.startswith("bytes=0-")):
log_file_access(full_path)
mime, _ = mimetypes.guess_type(full_path) mime, _ = mimetypes.guess_type(full_path)
mime = mime or 'application/octet-stream' mime = mime or 'application/octet-stream'
response = None if mime and mime.startswith('image/'):
pass # do not log access to images
else:
# HEAD request are coming in to initiate server caching.
# only log initial hits and not the reload of further file parts
range_header = request.headers.get('Range')
if request.method != 'HEAD' and (not range_header or range_header.startswith("bytes=0-")):
log_file_access(full_path)
# Check cache first (using diskcache) # Check cache first (using diskcache)
response = None
cached = cache.get(filename) cached = cache.get(filename)
if cached: if cached:
cached_file_bytes, mime = cached cached_file_bytes, mime = cached

View File

@ -16,11 +16,13 @@ SERVER1_MOUNT_POINTS=(
"/mnt/app.bethaus/Gottesdienste Speyer" "/mnt/app.bethaus/Gottesdienste Speyer"
"/mnt/app.bethaus/Besondere Gottesdienste" "/mnt/app.bethaus/Besondere Gottesdienste"
"/mnt/app.bethaus/Liedersammlung" "/mnt/app.bethaus/Liedersammlung"
"/mnt/app.share"
) )
SERVER1_NFS_SHARES=( SERVER1_NFS_SHARES=(
"/volume1/Aufnahme-stereo/010 Gottesdienste ARCHIV" "/volume1/Aufnahme-stereo/010 Gottesdienste ARCHIV"
"/volume1/Aufnahme-stereo/013 Besondere Gottesdienste" "/volume1/Aufnahme-stereo/013 Besondere Gottesdienste"
"/volume1/Aufnahme-stereo/014 Liedersammlung" "/volume1/Aufnahme-stereo/014 Liedersammlung"
"/volume1/app.share"
) )
# Server 2 Configuration # Server 2 Configuration

View File

@ -11,14 +11,18 @@ services:
- ./GeoLite2-City.mmdb:/app/GeoLite2-City.mmdb:ro - ./GeoLite2-City.mmdb:/app/GeoLite2-City.mmdb:ro
- type: bind - type: bind
source: /mnt/app.bethaus source: /mnt/app.bethaus
target: /mp3_root target: /main
bind:
propagation: rshared
- type: bind
source: /mnt/app.share
target: /share
bind: bind:
propagation: rshared propagation: rshared
environment: environment:
- FLASK_APP=app.py - FLASK_APP=app.py
- FLASK_RUN_HOST=0.0.0.0 - FLASK_RUN_HOST=0.0.0.0
- FLASK_ENV=production - FLASK_ENV=production
- MP3_ROOT=/mp3_root
networks: networks:
- traefik - traefik
labels: labels:

View File

@ -91,35 +91,6 @@
</div> </div>
</div> </div>
<!-- Detailed Table of Top File Accesses -->
<div class="card mb-4">
<div class="card-header">
Detailed File Access Data
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>File Path</th>
<th>Access Count</th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td>{{ row[0] }}</td>
<td>{{ row[1] }}</td>
</tr>
{% else %}
<tr>
<td colspan="2">No data available for the selected timeframe.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- New Section: IP Address Access Data --> <!-- New Section: IP Address Access Data -->
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
@ -152,13 +123,74 @@
</table> </table>
</div> </div>
</div> </div>
<!-- New Section: Aggregated City Access Data -->
<div class="card">
<div class="card-header">
City Access Distribution
</div>
<div class="card-body">
<table class="table table-bordered">
<thead>
<tr>
<th>City</th>
<th>Access Count</th>
</tr>
</thead>
<tbody>
{% for city in city_data %}
<tr>
<td>{{ city.city }}</td>
<td>{{ city.count }}</td>
</tr>
{% else %}
<tr>
<td colspan="2">No city access data available for the selected timeframe.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Detailed Table of Top File Accesses -->
<div class="card mb-4">
<div class="card-header">
Detailed File Access Data
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>File Path</th>
<th>Access Count</th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr>
<td>{{ row[0] }}</td>
<td>{{ row[1] }}</td>
</tr>
{% else %}
<tr>
<td colspan="2">No data available for the selected timeframe.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div> </div>
<!-- Load Chart.js from CDN --> <!-- Load Chart.js from CDN -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script> <script>
// Data passed from the backend as JSON // Data passed from the backend as JSON
const dailyAccessData = {{ daily_access_data|tojson }}; const dailyAccessData = {{ daily_access_data|tojson }};
const topFilesData = {{ top_files_data|tojson }}; const topFilesData = {{ top_files_data|tojson }};
// Note: user_agent_data now contains 'device' and 'count'
const userAgentData = {{ user_agent_data|tojson }}; const userAgentData = {{ user_agent_data|tojson }};
const referrerData = {{ referrer_data|tojson }}; const referrerData = {{ referrer_data|tojson }};
@ -208,12 +240,12 @@
} }
}); });
// User Agent Distribution - Pie Chart // User Agent Distribution - Pie Chart (using aggregated device data)
const ctxUserAgent = document.getElementById('userAgentChart').getContext('2d'); const ctxUserAgent = document.getElementById('userAgentChart').getContext('2d');
new Chart(ctxUserAgent, { new Chart(ctxUserAgent, {
type: 'pie', type: 'pie',
data: { data: {
labels: userAgentData.map(item => item.user_agent), labels: userAgentData.map(item => item.device),
datasets: [{ datasets: [{
data: userAgentData.map(item => item.count) data: userAgentData.map(item => item.count)
}] }]
@ -221,7 +253,7 @@
options: { responsive: true } options: { responsive: true }
}); });
// Referrer Distribution - Pie Chart // Referrer Distribution - Pie Chart (with shortened referrers)
const ctxReferrer = document.getElementById('referrerChart').getContext('2d'); const ctxReferrer = document.getElementById('referrerChart').getContext('2d');
new Chart(ctxReferrer, { new Chart(ctxReferrer, {
type: 'pie', type: 'pie',