make dashboard
This commit is contained in:
parent
e380ae25cb
commit
3fa915a9ce
BIN
GeoLite2-City.mmdb
Normal file
BIN
GeoLite2-City.mmdb
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 MiB |
108
app.py
108
app.py
@ -9,9 +9,12 @@ from datetime import datetime, date, timedelta
|
|||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
import diskcache
|
import diskcache
|
||||||
import json
|
import json
|
||||||
|
import geoip2.database
|
||||||
|
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= 32 * 1024**3) # 32 GB limit
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)
|
||||||
|
|
||||||
# Use a raw string for the default FILE_ROOT path.
|
# Use a raw string for the default FILE_ROOT path.
|
||||||
app.config['FILE_ROOT'] = r'/mp3_root'
|
app.config['FILE_ROOT'] = r'/mp3_root'
|
||||||
@ -199,16 +202,22 @@ def api_browse(subpath):
|
|||||||
'files': files
|
'files': files
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.route("/access-log")
|
def lookup_location(ip, reader):
|
||||||
|
try:
|
||||||
|
response = reader.city(ip)
|
||||||
|
country = response.country.name if response.country.name else "Unknown"
|
||||||
|
city = response.city.name if response.city.name else "Unknown"
|
||||||
|
return country, city
|
||||||
|
except Exception:
|
||||||
|
return "Unknown", "Unknown"
|
||||||
|
|
||||||
|
@app.route("/dashboard")
|
||||||
@require_secret
|
@require_secret
|
||||||
def access_log():
|
def dashboard():
|
||||||
# Get timeframe filter from query parameter; default to "today"
|
|
||||||
timeframe = request.args.get('timeframe', 'today')
|
timeframe = request.args.get('timeframe', 'today')
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
# Determine the start time based on the requested timeframe
|
|
||||||
if timeframe == 'today':
|
if timeframe == 'today':
|
||||||
# Beginning of today
|
|
||||||
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
elif timeframe == '7days':
|
elif timeframe == '7days':
|
||||||
start = now - timedelta(days=7)
|
start = now - timedelta(days=7)
|
||||||
@ -217,12 +226,12 @@ def access_log():
|
|||||||
elif timeframe == '365days':
|
elif timeframe == '365days':
|
||||||
start = now - timedelta(days=365)
|
start = now - timedelta(days=365)
|
||||||
else:
|
else:
|
||||||
# Default to today if an unknown timeframe is passed
|
|
||||||
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
# Query the access log database for file access counts since the 'start' time
|
|
||||||
conn = sqlite3.connect('access_log.db')
|
conn = sqlite3.connect('access_log.db')
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Raw file access counts for the table (top files)
|
||||||
cursor.execute('''
|
cursor.execute('''
|
||||||
SELECT full_path, COUNT(*) as access_count
|
SELECT full_path, COUNT(*) as access_count
|
||||||
FROM file_access_log
|
FROM file_access_log
|
||||||
@ -231,9 +240,86 @@ def access_log():
|
|||||||
ORDER BY access_count DESC
|
ORDER BY access_count DESC
|
||||||
''', (start.isoformat(),))
|
''', (start.isoformat(),))
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
# Daily access trend for a line chart
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT date(timestamp) as date, COUNT(*) as count
|
||||||
|
FROM file_access_log
|
||||||
|
WHERE timestamp >= ?
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date
|
||||||
|
''', (start.isoformat(),))
|
||||||
|
daily_access_data = [dict(date=row[0], count=row[1]) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Top files for bar chart (limit to 10)
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT full_path, COUNT(*) as access_count
|
||||||
|
FROM file_access_log
|
||||||
|
WHERE timestamp >= ?
|
||||||
|
GROUP BY full_path
|
||||||
|
ORDER BY access_count DESC
|
||||||
|
LIMIT 10
|
||||||
|
''', (start.isoformat(),))
|
||||||
|
top_files_data = [dict(full_path=row[0], access_count=row[1]) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# User agent distribution (limit to 10)
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT user_agent, COUNT(*) as count
|
||||||
|
FROM file_access_log
|
||||||
|
WHERE timestamp >= ?
|
||||||
|
GROUP BY user_agent
|
||||||
|
ORDER BY count DESC
|
||||||
|
LIMIT 10
|
||||||
|
''', (start.isoformat(),))
|
||||||
|
user_agent_data = [dict(user_agent=row[0], count=row[1]) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Referrer distribution (limit to 10)
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT referrer, COUNT(*) as count
|
||||||
|
FROM file_access_log
|
||||||
|
WHERE timestamp >= ?
|
||||||
|
GROUP BY referrer
|
||||||
|
ORDER BY count DESC
|
||||||
|
LIMIT 10
|
||||||
|
''', (start.isoformat(),))
|
||||||
|
referrer_data = [dict(referrer=row[0] if row[0] else "Direct/None", count=row[1]) for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
# Aggregate IP addresses with counts
|
||||||
|
cursor.execute('''
|
||||||
|
SELECT ip_address, COUNT(*) as count
|
||||||
|
FROM file_access_log
|
||||||
|
WHERE timestamp >= ?
|
||||||
|
GROUP BY ip_address
|
||||||
|
ORDER BY count DESC
|
||||||
|
''', (start.isoformat(),))
|
||||||
|
ip_rows = cursor.fetchall()
|
||||||
|
|
||||||
|
# Initialize GeoIP2 reader once for efficiency
|
||||||
|
reader = geoip2.database.Reader('GeoLite2-City.mmdb')
|
||||||
|
ip_data = []
|
||||||
|
for ip, count in ip_rows:
|
||||||
|
country, city = lookup_location(ip, reader)
|
||||||
|
ip_data.append(dict(ip=ip, count=count, country=country, city=city))
|
||||||
|
reader.close()
|
||||||
|
|
||||||
|
# Summary stats
|
||||||
|
total_accesses = sum([row[1] for row in rows])
|
||||||
|
unique_files = len(rows)
|
||||||
|
cursor.execute('SELECT COUNT(DISTINCT ip_address) FROM file_access_log WHERE timestamp >= ?', (start.isoformat(),))
|
||||||
|
unique_ips = cursor.fetchone()[0]
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
return render_template("access_log.html", rows=rows, timeframe=timeframe)
|
return render_template("dashboard.html",
|
||||||
|
timeframe=timeframe,
|
||||||
|
rows=rows,
|
||||||
|
daily_access_data=daily_access_data,
|
||||||
|
top_files_data=top_files_data,
|
||||||
|
user_agent_data=user_agent_data,
|
||||||
|
referrer_data=referrer_data,
|
||||||
|
ip_data=ip_data,
|
||||||
|
total_accesses=total_accesses,
|
||||||
|
unique_files=unique_files,
|
||||||
|
unique_ips=unique_ips)
|
||||||
|
|
||||||
def log_file_access(full_path):
|
def log_file_access(full_path):
|
||||||
"""
|
"""
|
||||||
@ -278,8 +364,10 @@ 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
|
||||||
|
|
||||||
# Exclude HEAD requests from logging
|
# HEAD request are coming in to initiate server caching.
|
||||||
if request.method != 'HEAD':
|
# 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)
|
log_file_access(full_path)
|
||||||
|
|
||||||
mime, _ = mimetypes.guess_type(full_path)
|
mime, _ = mimetypes.guess_type(full_path)
|
||||||
|
|||||||
@ -8,7 +8,12 @@ services:
|
|||||||
- ./:/app
|
- ./:/app
|
||||||
- ./filecache:/app/filecache
|
- ./filecache:/app/filecache
|
||||||
- ./templates:/app/templates
|
- ./templates:/app/templates
|
||||||
- /mnt/app.bethaus:/mp3_root:ro
|
- ./GeoLite2-City.mmdb:/app/GeoLite2-City.mmdb:ro
|
||||||
|
- type: bind
|
||||||
|
source: /mnt/app.bethaus
|
||||||
|
target: /mp3_root
|
||||||
|
bind:
|
||||||
|
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
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
flask
|
flask
|
||||||
pillow
|
pillow
|
||||||
diskcache
|
diskcache
|
||||||
|
geoip2
|
||||||
|
|||||||
@ -119,6 +119,8 @@ function attachEventListeners() {
|
|||||||
const newPath = this.getAttribute('data-path');
|
const newPath = this.getAttribute('data-path');
|
||||||
loadDirectory(newPath);
|
loadDirectory(newPath);
|
||||||
history.pushState({ subpath: newPath }, '', newPath ? '/path/' + newPath : '/');
|
history.pushState({ subpath: newPath }, '', newPath ? '/path/' + newPath : '/');
|
||||||
|
// Scroll to the top of the page after click:
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -15,12 +15,12 @@ body {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr auto;
|
grid-template-rows: auto 1fr auto;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
padding-bottom: 200px;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-bottom: 200px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container a {
|
.container a {
|
||||||
|
|||||||
@ -1,46 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>File Access Log</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
|
||||||
table, th, td { border: 1px solid #ccc; border-collapse: collapse; padding: 8px; }
|
|
||||||
th { background-color: #f2f2f2; }
|
|
||||||
.btn { margin: 5px; padding: 8px 12px; text-decoration: none; background-color: #4CAF50; color: white; border-radius: 4px; }
|
|
||||||
.btn:hover { background-color: #45a049; }
|
|
||||||
.box { margin: 15px 0; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>File Access Log ({{ timeframe }})</h1>
|
|
||||||
<div class="box">
|
|
||||||
<a href="{{ url_for('access_log', timeframe='today') }}" class="btn">Today</a>
|
|
||||||
<a href="{{ url_for('access_log', timeframe='7days') }}" class="btn">Last 7 Days</a>
|
|
||||||
<a href="{{ url_for('access_log', timeframe='30days') }}" class="btn">Last 30 Days</a>
|
|
||||||
<a href="{{ url_for('access_log', timeframe='365days') }}" class="btn">Last 365 Days</a>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<table>
|
|
||||||
<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>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
238
templates/dashboard.html
Normal file
238
templates/dashboard.html
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Web Analytics Dashboard</title>
|
||||||
|
<!-- Using Bootstrap for responsive layout -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body { margin: 20px; }
|
||||||
|
.card { margin-bottom: 20px; }
|
||||||
|
canvas { max-width: 100%; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="mb-4">Web Analytics Dashboard ({{ timeframe }})</h1>
|
||||||
|
<!-- Timeframe selection buttons -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<a href="{{ url_for('index') }}" class="btn btn-primary">Home</a>
|
||||||
|
<a href="{{ url_for('dashboard', timeframe='today') }}" class="btn btn-primary">Today</a>
|
||||||
|
<a href="{{ url_for('dashboard', timeframe='7days') }}" class="btn btn-primary">Last 7 Days</a>
|
||||||
|
<a href="{{ url_for('dashboard', timeframe='30days') }}" class="btn btn-primary">Last 30 Days</a>
|
||||||
|
<a href="{{ url_for('dashboard', timeframe='365days') }}" class="btn btn-primary">Last 365 Days</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary Cards -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-white bg-info">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Total Accesses</h5>
|
||||||
|
<p class="card-text">{{ total_accesses }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-white bg-success">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Unique Files</h5>
|
||||||
|
<p class="card-text">{{ unique_files }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card text-white bg-warning">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Unique IPs</h5>
|
||||||
|
<p class="card-text">{{ unique_ips }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Charts Section -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Access Trend Chart -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Access Trend</h5>
|
||||||
|
<canvas id="accessTrendChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Top Files Accessed Chart -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Top Files Accessed</h5>
|
||||||
|
<canvas id="topFilesChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- User Agent Distribution Chart -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">User Agent Distribution</h5>
|
||||||
|
<canvas id="userAgentChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Referrer Distribution Chart -->
|
||||||
|
<div class="col-md-6 mb-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Referrer Distribution</h5>
|
||||||
|
<canvas id="referrerChart"></canvas>
|
||||||
|
</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 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
IP Address Access Distribution
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>IP Address</th>
|
||||||
|
<th>Access Count</th>
|
||||||
|
<th>City</th>
|
||||||
|
<th>Country</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for ip in ip_data %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ ip.ip }}</td>
|
||||||
|
<td>{{ ip.count }}</td>
|
||||||
|
<td>{{ ip.city }}</td>
|
||||||
|
<td>{{ ip.country }}</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="4">No IP access data available for the selected timeframe.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Load Chart.js from CDN -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
// Data passed from the backend as JSON
|
||||||
|
const dailyAccessData = {{ daily_access_data|tojson }};
|
||||||
|
const topFilesData = {{ top_files_data|tojson }};
|
||||||
|
const userAgentData = {{ user_agent_data|tojson }};
|
||||||
|
const referrerData = {{ referrer_data|tojson }};
|
||||||
|
|
||||||
|
// Access Trend Chart - Line Chart
|
||||||
|
const ctxTrend = document.getElementById('accessTrendChart').getContext('2d');
|
||||||
|
new Chart(ctxTrend, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: dailyAccessData.map(item => item.date),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Access Count',
|
||||||
|
data: dailyAccessData.map(item => item.count),
|
||||||
|
borderWidth: 2,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: { legend: { position: 'top' } },
|
||||||
|
scales: {
|
||||||
|
x: { title: { display: true, text: 'Date' } },
|
||||||
|
y: { title: { display: true, text: 'Access Count' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Top Files Chart - Horizontal Bar Chart
|
||||||
|
const ctxTopFiles = document.getElementById('topFilesChart').getContext('2d');
|
||||||
|
new Chart(ctxTopFiles, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: topFilesData.map(item => item.full_path),
|
||||||
|
datasets: [{
|
||||||
|
label: 'Access Count',
|
||||||
|
data: topFilesData.map(item => item.access_count),
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
responsive: true,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
x: { title: { display: true, text: 'Access Count' } },
|
||||||
|
y: { title: { display: true, text: 'File Path' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// User Agent Distribution - Pie Chart
|
||||||
|
const ctxUserAgent = document.getElementById('userAgentChart').getContext('2d');
|
||||||
|
new Chart(ctxUserAgent, {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: userAgentData.map(item => item.user_agent),
|
||||||
|
datasets: [{
|
||||||
|
data: userAgentData.map(item => item.count)
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: { responsive: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Referrer Distribution - Pie Chart
|
||||||
|
const ctxReferrer = document.getElementById('referrerChart').getContext('2d');
|
||||||
|
new Chart(ctxReferrer, {
|
||||||
|
type: 'pie',
|
||||||
|
data: {
|
||||||
|
labels: referrerData.map(item => item.referrer),
|
||||||
|
datasets: [{
|
||||||
|
data: referrerData.map(item => item.count)
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: { responsive: true }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user