Dashboard mit Statistiken

Dieser Commit ist enthalten in:
2025-06-07 13:59:32 +02:00
Ursprung 91a193e0fb
Commit 68d6cc346b
8 geänderte Dateien mit 525 neuen und 5 gelöschten Zeilen

Datei anzeigen

@@ -194,3 +194,32 @@ Lizenzmanagement-System für Social Media Account-Erstellungssoftware mit Docker
- Lösch-Button ist deaktiviert, wenn Kunde Lizenzen hat
- Aktive Lizenzen werden separat gezählt (nicht abgelaufen + aktiv)
- UTF-8 Support für Kundennamen mit Umlauten
### 2025-01-06 - Dashboard mit Statistiken implementiert
- Übersichtliches Dashboard als neue Startseite
- Statistik-Karten mit wichtigen Kennzahlen
- Listen für bald ablaufende und zuletzt erstellte Lizenzen
- Routing angepasst: Dashboard (/) und Lizenz erstellen (/create)
**Neue Features:**
- Statistik-Karten: Kunden, Lizenzen gesamt, Aktive, Ablaufende
- Aufteilung nach Lizenztypen (Vollversion/Testversion)
- Aufteilung nach Status (Aktiv/Abgelaufen)
- Top 10 bald ablaufende Lizenzen mit Restlaufzeit
- Letzte 5 erstellte Lizenzen mit Status
- Hover-Effekt auf Statistik-Karten
- Einheitliche Navigation mit Dashboard-Link
**Geänderte/Neue Dateien:**
- v2_adminpanel/app.py (dashboard() komplett überarbeitet, create_license() Route)
- v2_adminpanel/templates/dashboard.html (neu erstellt)
- v2_adminpanel/templates/index.html (Navigation erweitert)
- v2_adminpanel/templates/licenses.html (Navigation angepasst)
- v2_adminpanel/templates/customers.html (Navigation angepasst)
**Dashboard-Inhalte:**
- 4 Hauptstatistiken als Karten
- Lizenztyp-Verteilung
- Status-Verteilung
- Warnung für bald ablaufende Lizenzen
- Übersicht der neuesten Aktivitäten

Datei anzeigen

@@ -64,9 +64,101 @@ def logout():
session.pop('username', None)
return redirect(url_for('login'))
@app.route("/", methods=["GET", "POST"])
@app.route("/")
@login_required
def dashboard():
conn = get_connection()
cur = conn.cursor()
# Statistiken abrufen
# Gesamtanzahl Kunden
cur.execute("SELECT COUNT(*) FROM customers")
total_customers = cur.fetchone()[0]
# Gesamtanzahl Lizenzen
cur.execute("SELECT COUNT(*) FROM licenses")
total_licenses = cur.fetchone()[0]
# Aktive Lizenzen (nicht abgelaufen und is_active = true)
cur.execute("""
SELECT COUNT(*) FROM licenses
WHERE valid_until >= CURRENT_DATE AND is_active = TRUE
""")
active_licenses = cur.fetchone()[0]
# Abgelaufene Lizenzen
cur.execute("""
SELECT COUNT(*) FROM licenses
WHERE valid_until < CURRENT_DATE
""")
expired_licenses = cur.fetchone()[0]
# Lizenzen die in den nächsten 30 Tagen ablaufen
cur.execute("""
SELECT COUNT(*) FROM licenses
WHERE valid_until >= CURRENT_DATE
AND valid_until < CURRENT_DATE + INTERVAL '30 days'
AND is_active = TRUE
""")
expiring_soon = cur.fetchone()[0]
# Testlizenzen vs Vollversionen
cur.execute("""
SELECT license_type, COUNT(*)
FROM licenses
GROUP BY license_type
""")
license_types = dict(cur.fetchall())
# Letzte 5 erstellten Lizenzen
cur.execute("""
SELECT l.id, l.license_key, c.name, l.valid_until,
CASE
WHEN l.valid_until < CURRENT_DATE THEN 'abgelaufen'
WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'läuft bald ab'
ELSE 'aktiv'
END as status
FROM licenses l
JOIN customers c ON l.customer_id = c.id
ORDER BY l.id DESC
LIMIT 5
""")
recent_licenses = cur.fetchall()
# Bald ablaufende Lizenzen (nächste 30 Tage)
cur.execute("""
SELECT l.id, l.license_key, c.name, l.valid_until,
l.valid_until - CURRENT_DATE as days_left
FROM licenses l
JOIN customers c ON l.customer_id = c.id
WHERE l.valid_until >= CURRENT_DATE
AND l.valid_until < CURRENT_DATE + INTERVAL '30 days'
AND l.is_active = TRUE
ORDER BY l.valid_until
LIMIT 10
""")
expiring_licenses = cur.fetchall()
cur.close()
conn.close()
stats = {
'total_customers': total_customers,
'total_licenses': total_licenses,
'active_licenses': active_licenses,
'expired_licenses': expired_licenses,
'expiring_soon': expiring_soon,
'full_licenses': license_types.get('full', 0),
'test_licenses': license_types.get('test', 0),
'recent_licenses': recent_licenses,
'expiring_licenses': expiring_licenses
}
return render_template("dashboard.html", stats=stats, username=session.get('username'))
@app.route("/create", methods=["GET", "POST"])
@login_required
def create_license():
if request.method == "POST":
name = request.form["customer_name"]
email = request.form["email"]
@@ -96,7 +188,7 @@ def dashboard():
cur.close()
conn.close()
return redirect("/")
return redirect("/create")
return render_template("index.html", username=session.get('username'))

Datei anzeigen

@@ -20,7 +20,8 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Kundenverwaltung</h2>
<div>
<a href="/" class="btn btn-primary"> Neue Lizenz</a>
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
<a href="/create" class="btn btn-primary"> Neue Lizenz</a>
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
</div>
</div>

Datei anzeigen

@@ -0,0 +1,199 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Dashboard - Admin Panel</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.stat-card {
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-5px);
}
.status-aktiv { color: #28a745; }
.status-ablaufend { color: #ffc107; }
.status-abgelaufen { color: #dc3545; }
</style>
</head>
<body class="bg-light">
<nav class="navbar navbar-dark bg-dark">
<div class="container">
<span class="navbar-brand">🎛️ Lizenzverwaltung</span>
<div class="d-flex align-items-center">
<span class="text-white me-3">Angemeldet als: {{ username }}</span>
<a href="/logout" class="btn btn-outline-light btn-sm">Abmelden</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Dashboard</h1>
<div>
<a href="/create" class="btn btn-primary"> Neue Lizenz</a>
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
</div>
</div>
<!-- Statistik-Karten -->
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="card stat-card h-100">
<div class="card-body text-center">
<h5 class="card-title">👥 Kunden</h5>
<h2 class="text-primary">{{ stats.total_customers }}</h2>
<p class="text-muted mb-0">Gesamt</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card h-100">
<div class="card-body text-center">
<h5 class="card-title">📋 Lizenzen</h5>
<h2 class="text-info">{{ stats.total_licenses }}</h2>
<p class="text-muted mb-0">Gesamt</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card h-100">
<div class="card-body text-center">
<h5 class="card-title">✅ Aktiv</h5>
<h2 class="text-success">{{ stats.active_licenses }}</h2>
<p class="text-muted mb-0">Gültige Lizenzen</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card h-100">
<div class="card-body text-center">
<h5 class="card-title">⚠️ Ablaufend</h5>
<h2 class="text-warning">{{ stats.expiring_soon }}</h2>
<p class="text-muted mb-0">Nächste 30 Tage</p>
</div>
</div>
</div>
</div>
<!-- Lizenztypen -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Lizenztypen</h5>
<div class="row">
<div class="col-6 text-center">
<h3 class="text-success">{{ stats.full_licenses }}</h3>
<p class="text-muted">Vollversionen</p>
</div>
<div class="col-6 text-center">
<h3 class="text-warning">{{ stats.test_licenses }}</h3>
<p class="text-muted">Testversionen</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">Lizenzstatus</h5>
<div class="row">
<div class="col-6 text-center">
<h3 class="text-success">{{ stats.active_licenses }}</h3>
<p class="text-muted">Aktiv</p>
</div>
<div class="col-6 text-center">
<h3 class="text-danger">{{ stats.expired_licenses }}</h3>
<p class="text-muted">Abgelaufen</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row g-3">
<!-- Bald ablaufende Lizenzen -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0">⏰ Bald ablaufende Lizenzen</h5>
</div>
<div class="card-body">
{% if stats.expiring_licenses %}
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Kunde</th>
<th>Lizenz</th>
<th>Tage</th>
</tr>
</thead>
<tbody>
{% for license in stats.expiring_licenses %}
<tr>
<td>{{ license[2] }}</td>
<td><small><code>{{ license[1][:8] }}...</code></small></td>
<td><span class="badge bg-warning">{{ license[4] }} Tage</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted mb-0">Keine Lizenzen laufen in den nächsten 30 Tagen ab.</p>
{% endif %}
</div>
</div>
</div>
<!-- Letzte Lizenzen -->
<div class="col-md-6">
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="mb-0">🆕 Zuletzt erstellte Lizenzen</h5>
</div>
<div class="card-body">
{% if stats.recent_licenses %}
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>Kunde</th>
<th>Lizenz</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for license in stats.recent_licenses %}
<tr>
<td>{{ license[2] }}</td>
<td><small><code>{{ license[1][:8] }}...</code></small></td>
<td>
{% if license[4] == 'abgelaufen' %}
<span class="status-abgelaufen">⚠️ Abgelaufen</span>
{% elif license[4] == 'läuft bald ab' %}
<span class="status-ablaufend">⏰ Läuft bald ab</span>
{% else %}
<span class="status-aktiv">✅ Aktiv</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted mb-0">Noch keine Lizenzen erstellt.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</body>
</html>

Datei anzeigen

@@ -20,6 +20,7 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Neue Lizenz erstellen</h2>
<div>
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
<a href="/licenses" class="btn btn-secondary">📋 Lizenzen</a>
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
</div>

Datei anzeigen

@@ -25,7 +25,8 @@
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Lizenzübersicht</h2>
<div>
<a href="/" class="btn btn-primary"> Neue Lizenz</a>
<a href="/" class="btn btn-secondary">📊 Dashboard</a>
<a href="/create" class="btn btn-primary"> Neue Lizenz</a>
<a href="/customers" class="btn btn-secondary">👥 Kunden</a>
</div>
</div>

144
v2_testing/test_dashboard.py Normale Datei
Datei anzeigen

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
import requests
import urllib3
import subprocess
# Disable SSL warnings for self-signed certificate
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Test configuration
base_url = "https://localhost:443"
admin_user = {"username": "rac00n", "password": "1248163264"}
def login(session):
"""Login to admin panel"""
login_data = {
"username": admin_user["username"],
"password": admin_user["password"]
}
response = session.post(f"{base_url}/login", data=login_data, verify=False, allow_redirects=False)
return response.status_code == 302
def test_dashboard():
"""Test dashboard with statistics"""
session = requests.Session()
if not login(session):
return "✗ Failed to login"
response = session.get(f"{base_url}/", verify=False)
if response.status_code != 200:
return f"✗ Failed to access dashboard: Status {response.status_code}"
content = response.text
results = []
# Check for statistics elements
statistics_checks = [
("Gesamtkunden", "Total customers statistic"),
("Gesamtlizenzen", "Total licenses statistic"),
("Aktive Lizenzen", "Active licenses statistic"),
("Abgelaufene Lizenzen", "Expired licenses statistic"),
("Läuft bald ab", "Expiring soon statistic"),
("Letzte Lizenzen", "Recent licenses section"),
("Bald ablaufende Lizenzen", "Expiring licenses section")
]
for check_text, description in statistics_checks:
if check_text in content:
results.append(f"✓ Found: {description}")
else:
results.append(f"✗ Missing: {description}")
# Check if actual numbers are displayed
if any(char.isdigit() for char in content):
results.append("✓ Statistics numbers are displayed")
else:
results.append("✗ No statistics numbers found")
# Check for customer names in recent licenses
if "Müller" in content or "Schröder" in content or "Köhler" in content:
results.append("✓ Recent licenses show customer names")
else:
results.append("✗ No customer names in recent licenses")
return results
# Rebuild and restart admin panel
print("Rebuilding admin panel with dashboard...")
subprocess.run(["docker-compose", "build", "admin-panel"], capture_output=True)
subprocess.run(["docker-compose", "up", "-d", "admin-panel"], capture_output=True)
print("Waiting for container to start...")
subprocess.run(["sleep", "5"], capture_output=True)
print("\nTesting Dashboard with Statistics")
print("=" * 50)
results = test_dashboard()
for result in results:
print(result)
# Get actual statistics from database for comparison
print("\n" + "=" * 50)
print("Actual Database Statistics:")
print("-" * 50)
# Total customers
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank", "-t",
"-c", "SELECT COUNT(*) FROM customers;"
], capture_output=True, text=True)
print(f"Total Customers: {result.stdout.strip()}")
# Total licenses
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank", "-t",
"-c", "SELECT COUNT(*) FROM licenses;"
], capture_output=True, text=True)
print(f"Total Licenses: {result.stdout.strip()}")
# Active licenses
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank", "-t",
"-c", "SELECT COUNT(*) FROM licenses WHERE valid_until >= CURRENT_DATE AND is_active = TRUE;"
], capture_output=True, text=True)
print(f"Active Licenses: {result.stdout.strip()}")
# Expired licenses
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank", "-t",
"-c", "SELECT COUNT(*) FROM licenses WHERE valid_until < CURRENT_DATE;"
], capture_output=True, text=True)
print(f"Expired Licenses: {result.stdout.strip()}")
# Expiring soon (30 days)
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank", "-t",
"-c", "SELECT COUNT(*) FROM licenses WHERE valid_until >= CURRENT_DATE AND valid_until < CURRENT_DATE + INTERVAL '30 days' AND is_active = TRUE;"
], capture_output=True, text=True)
print(f"Expiring Soon (30 days): {result.stdout.strip()}")
# License types breakdown
print("\nLicense Types:")
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank",
"-c", "SELECT license_type, COUNT(*) FROM licenses GROUP BY license_type ORDER BY COUNT(*) DESC;"
], capture_output=True, text=True)
print(result.stdout)
# Recent licenses
print("Recent Licenses:")
result = subprocess.run([
"docker", "exec", "db", "psql", "-U", "adminuser", "-d", "meinedatenbank",
"-c", """SELECT l.license_key, c.name, l.valid_until,
CASE
WHEN l.valid_until < CURRENT_DATE THEN 'abgelaufen'
WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'läuft bald ab'
ELSE 'aktiv'
END as status
FROM licenses l
JOIN customers c ON l.customer_id = c.id
ORDER BY l.id DESC
LIMIT 5;"""
], capture_output=True, text=True)
print(result.stdout)

Datei anzeigen

@@ -0,0 +1,53 @@
#!/usr/bin/env python3
import requests
import urllib3
import re
# Disable SSL warnings for self-signed certificate
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Test configuration
base_url = "https://localhost:443"
admin_user = {"username": "rac00n", "password": "1248163264"}
def test_dashboard_detail():
"""Test dashboard content in detail"""
session = requests.Session()
# Login
login_data = {
"username": admin_user["username"],
"password": admin_user["password"]
}
session.post(f"{base_url}/login", data=login_data, verify=False, allow_redirects=False)
# Get dashboard
response = session.get(f"{base_url}/", verify=False)
content = response.text
# Extract numbers from content
numbers = re.findall(r'\d+', content)
print(f"Numbers found on page: {numbers[:20]}") # First 20 numbers
# Check specific sections
if "8" in numbers: # Total customers
print("✓ Found total customers count (8)")
if "5" in numbers: # Total licenses
print("✓ Found total licenses count (5)")
if "3" in numbers: # Active licenses
print("✓ Found active licenses count (3)")
if "2" in numbers: # Expired licenses
print("✓ Found expired licenses count (2)")
# Print a snippet of the HTML to see structure
print("\nHTML snippet (first 1000 chars):")
print(content[:1000])
return response.status_code
print("Detailed Dashboard Test")
print("=" * 50)
status = test_dashboard_detail()