Vorläufig fertiger server

Dieser Commit ist enthalten in:
2025-06-19 13:17:24 +02:00
Ursprung c30d974d57
Commit f82131b5f9
19 geänderte Dateien mit 1595 neuen und 583 gelöschten Zeilen

Datei anzeigen

@@ -1,5 +1,45 @@
# v2-Docker Projekt Journal
## Letzte Änderungen (19.06.2025 - 12:46 Uhr)
### Navigation komplett überarbeitet
- **500 Fehler auf /live-dashboard behoben**:
- Datenbankabfrage korrigiert (falsche Spaltenreferenz `c.contact_person` entfernt)
- Alle Referenzen nutzen jetzt korrekt `c.name as company_name`
- **Navigation aufgeräumt und reorganisiert**:
- "Admin Sessions" komplett entfernt (nicht benötigt)
- "Alerts & Anomalien" entfernt (redundant zu "Lizenz-Anomalien")
- "Lizenzserver Status" und "Analytics" zu einer Seite zusammengeführt
- "Live Dashboard" aus Submenu entfernt (Monitoring führt direkt dorthin)
- Neuer "Administration" Bereich erstellt (führt zu Lizenzserver Config)
- **Neue Navigationsstruktur**:
```
📊 Monitoring → (Live Dashboard)
├── System Status
├── Lizenz-Anomalien
└── Analytics (kombiniert)
🔧 Administration → (Lizenzserver Config)
├── Audit-Log
├── Backups
└── Gesperrte IPs
```
- **Weitere Verbesserungen**:
- Grafana-Link aus System Status entfernt
- Session-Route-Fehler behoben (`admin.sessions` → `sessions.sessions`)
- Klarere Trennung zwischen operativem Monitoring und Admin-Tools
### Status:
✅ Navigation ist jetzt intuitiv und aufgeräumt
✅ Alle 500 Fehler behoben
✅ Redundante Menüpunkte eliminiert
✅ Admin-Tools klar von Monitoring getrennt
---
## Letzte Änderungen (19.06.2025)
### Monitoring vereinfacht und optimiert

Datei anzeigen

@@ -685,16 +685,44 @@ Neuer Menüpunkt "Lizenzserver" mit folgenden Unterseiten:
- Response Time Monitoring
- Keine externen Dependencies
3. **Alerts** (/monitoring/alerts)
- Anomalie-Erkennung aus DB
- Ungelöste Probleme
- Echtzeit-Updates
4. **Analytics** (/monitoring/analytics)
3. **Analytics** (/monitoring/analytics)
- Kombiniert aus Lizenzserver Status + Analytics
- Echte Statistiken statt Demo-Daten
- Auto-Refresh alle 30 Sekunden
- Basis-Metriken ohne Pricing
### ✅ UPDATE: Navigation komplett überarbeitet (19.06.2025 - 12:46 Uhr)
**Verbesserungen der Admin Panel Navigation**:
1. **Fehler behoben**:
- 500 Fehler auf /live-dashboard durch korrigierte DB-Abfragen
- Session-Route-Referenzen korrigiert
2. **Navigation aufgeräumt**:
- "Admin Sessions" entfernt (nicht benötigt)
- "Alerts & Anomalien" entfernt (redundant)
- "Lizenzserver Status" + "Analytics" zusammengeführt
- Grafana-Links entfernt
3. **Neue Struktur**:
```
📊 Monitoring → (Live Dashboard)
├── System Status
├── Lizenz-Anomalien
└── Analytics (kombiniert)
🔧 Administration → (Lizenzserver Config)
├── Audit-Log
├── Backups
└── Gesperrte IPs
```
4. **Vorteile**:
- Intuitive Navigation ohne Redundanzen
- Klare Trennung: Monitoring vs. Administration
- Direkte Links zu Hauptfunktionen (kein unnötiges Klicken)
### 📋 Noch zu implementieren:
1. **Erweiterte Anomalie-Erkennung**

Datei anzeigen

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
"""
Apply the license_heartbeats table migration
"""
import os
import psycopg2
import logging
from datetime import datetime
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def get_db_connection():
"""Get database connection"""
return psycopg2.connect(
host=os.environ.get('POSTGRES_HOST', 'postgres'),
database=os.environ.get('POSTGRES_DB', 'v2_adminpanel'),
user=os.environ.get('POSTGRES_USER', 'postgres'),
password=os.environ.get('POSTGRES_PASSWORD', 'postgres')
)
def apply_migration():
"""Apply the license_heartbeats migration"""
conn = None
try:
logger.info("Connecting to database...")
conn = get_db_connection()
cur = conn.cursor()
# Read migration file
migration_file = os.path.join(os.path.dirname(__file__), 'migrations', 'create_license_heartbeats_table.sql')
logger.info(f"Reading migration file: {migration_file}")
with open(migration_file, 'r') as f:
migration_sql = f.read()
# Execute migration
logger.info("Executing migration...")
cur.execute(migration_sql)
# Verify table was created
cur.execute("""
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = 'license_heartbeats'
)
""")
if cur.fetchone()[0]:
logger.info("✓ license_heartbeats table created successfully!")
# Check partitions
cur.execute("""
SELECT tablename
FROM pg_tables
WHERE tablename LIKE 'license_heartbeats_%'
ORDER BY tablename
""")
partitions = cur.fetchall()
logger.info(f"✓ Created {len(partitions)} partitions:")
for partition in partitions:
logger.info(f" - {partition[0]}")
else:
logger.error("✗ Failed to create license_heartbeats table")
return False
conn.commit()
logger.info("✓ Migration completed successfully!")
return True
except Exception as e:
logger.error(f"✗ Migration failed: {str(e)}")
if conn:
conn.rollback()
return False
finally:
if conn:
cur.close()
conn.close()
if __name__ == "__main__":
logger.info("=== Applying license_heartbeats migration ===")
logger.info(f"Timestamp: {datetime.now()}")
if apply_migration():
logger.info("=== Migration successful! ===")
else:
logger.error("=== Migration failed! ===")
exit(1)

Datei anzeigen

@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
Apply partition migration for license_heartbeats table.
This script creates missing partitions for the current and future months.
"""
import psycopg2
import os
import sys
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
def get_db_connection():
"""Get database connection"""
return psycopg2.connect(
host=os.environ.get('POSTGRES_HOST', 'postgres'),
database=os.environ.get('POSTGRES_DB', 'v2_adminpanel'),
user=os.environ.get('POSTGRES_USER', 'postgres'),
password=os.environ.get('POSTGRES_PASSWORD', 'postgres')
)
def create_partition(cursor, year, month):
"""Create a partition for the given year and month"""
partition_name = f"license_heartbeats_{year}_{month:02d}"
start_date = f"{year}-{month:02d}-01"
# Calculate end date (first day of next month)
if month == 12:
end_date = f"{year + 1}-01-01"
else:
end_date = f"{year}-{month + 1:02d}-01"
# Check if partition already exists
cursor.execute("""
SELECT EXISTS (
SELECT 1
FROM pg_tables
WHERE tablename = %s
)
""", (partition_name,))
exists = cursor.fetchone()[0]
if not exists:
try:
cursor.execute(f"""
CREATE TABLE {partition_name} PARTITION OF license_heartbeats
FOR VALUES FROM ('{start_date}') TO ('{end_date}')
""")
print(f"✓ Created partition {partition_name}")
return True
except Exception as e:
print(f"✗ Error creating partition {partition_name}: {e}")
return False
else:
print(f"- Partition {partition_name} already exists")
return False
def main():
"""Main function"""
print("Applying license_heartbeats partition migration...")
print("-" * 50)
try:
# Connect to database
conn = get_db_connection()
cursor = conn.cursor()
# Check if license_heartbeats table exists
cursor.execute("""
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = 'license_heartbeats'
)
""")
if not cursor.fetchone()[0]:
print("✗ Error: license_heartbeats table does not exist!")
print(" Please run the init.sql script first.")
return 1
# Get current date
current_date = datetime.now()
partitions_created = 0
# Create partitions for the next 6 months (including current month)
for i in range(7):
target_date = current_date + relativedelta(months=i)
if create_partition(cursor, target_date.year, target_date.month):
partitions_created += 1
# Commit changes
conn.commit()
print("-" * 50)
print(f"✓ Migration complete. Created {partitions_created} new partitions.")
# List all partitions
cursor.execute("""
SELECT tablename
FROM pg_tables
WHERE tablename LIKE 'license_heartbeats_%'
ORDER BY tablename
""")
partitions = cursor.fetchall()
print(f"\nTotal partitions: {len(partitions)}")
for partition in partitions:
print(f" - {partition[0]}")
cursor.close()
conn.close()
return 0
except Exception as e:
print(f"✗ Error: {e}")
return 1
if __name__ == "__main__":
sys.exit(main())

Datei anzeigen

@@ -0,0 +1,77 @@
# License Heartbeats Partition Management
## Overview
The `license_heartbeats` table uses PostgreSQL partitioning to efficiently store and query time-series data. Each partition contains data for one month.
## Problem
The monitoring dashboard may fail with "relation does not exist" errors when:
1. The current month's partition hasn't been created
2. The license_heartbeats table hasn't been properly initialized
## Solution
### Automatic Partition Creation
The monitoring routes now automatically:
1. Check if the `license_heartbeats` table exists
2. Create missing partitions for the current month
3. Handle missing table/partition gracefully by returning empty data
### Manual Partition Creation
If needed, you can manually create partitions:
1. **Using the migration script:**
```bash
docker exec -it v2_adminpanel python apply_partition_migration.py
```
2. **Using SQL directly:**
```sql
-- Create June 2025 partition
CREATE TABLE IF NOT EXISTS license_heartbeats_2025_06 PARTITION OF license_heartbeats
FOR VALUES FROM ('2025-06-01') TO ('2025-07-01');
```
### Partition Naming Convention
Partitions follow the naming pattern: `license_heartbeats_YYYY_MM`
Examples:
- `license_heartbeats_2025_01` - January 2025
- `license_heartbeats_2025_06` - June 2025
- `license_heartbeats_2025_12` - December 2025
### Best Practices
1. **Pre-create partitions**: Create partitions for the next 6 months to avoid runtime issues
2. **Monitor partition usage**: Check which partitions exist regularly
3. **Archive old data**: Consider dropping very old partitions to save space
### Troubleshooting
If you see "relation does not exist" errors:
1. Check if the base table exists:
```sql
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_name = 'license_heartbeats'
);
```
2. List existing partitions:
```sql
SELECT tablename FROM pg_tables
WHERE tablename LIKE 'license_heartbeats_%'
ORDER BY tablename;
```
3. Create missing partition for current month:
```sql
-- Replace YYYY_MM with current year and month
CREATE TABLE IF NOT EXISTS license_heartbeats_YYYY_MM PARTITION OF license_heartbeats
FOR VALUES FROM ('YYYY-MM-01') TO ('YYYY-MM-01'::date + interval '1 month');
```

Datei anzeigen

@@ -394,6 +394,10 @@ CREATE TABLE IF NOT EXISTS license_heartbeats_2025_01 PARTITION OF license_heart
CREATE TABLE IF NOT EXISTS license_heartbeats_2025_02 PARTITION OF license_heartbeats
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
-- Add June 2025 partition for current month
CREATE TABLE IF NOT EXISTS license_heartbeats_2025_06 PARTITION OF license_heartbeats
FOR VALUES FROM ('2025-06-01') TO ('2025-07-01');
CREATE INDEX idx_heartbeat_license_time ON license_heartbeats(license_id, timestamp DESC);
CREATE INDEX idx_heartbeat_hardware_time ON license_heartbeats(hardware_id, timestamp DESC);

Datei anzeigen

@@ -0,0 +1,75 @@
# Database Migrations
## License Heartbeats Table Migration
### Overview
The `create_license_heartbeats_table.sql` migration creates a partitioned table for storing real-time license validation data. This table is essential for the Live Dashboard & Analytics functionality.
### Features
- Monthly partitioning for efficient data management
- Automatic creation of current and next month partitions
- Optimized indexes for performance
- Foreign key relationship with licenses table
### Running the Migration
#### Option 1: Using the Python Script
```bash
cd /path/to/v2_adminpanel
python apply_license_heartbeats_migration.py
```
#### Option 2: Manual SQL Execution
```bash
psql -h postgres -U postgres -d v2_adminpanel -f migrations/create_license_heartbeats_table.sql
```
#### Option 3: Docker Exec
```bash
docker exec -it v2_adminpanel_postgres psql -U postgres -d v2_adminpanel -f /migrations/create_license_heartbeats_table.sql
```
### Verification
After running the migration, verify the table was created:
```sql
-- Check if table exists
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = 'license_heartbeats'
);
-- List all partitions
SELECT tablename
FROM pg_tables
WHERE tablename LIKE 'license_heartbeats_%'
ORDER BY tablename;
```
### Partition Management
The system automatically creates partitions as needed. To manually create future partitions:
```python
from utils.partition_helper import create_future_partitions
import psycopg2
conn = psycopg2.connect(...)
create_future_partitions(conn, 'license_heartbeats', months_ahead=6)
```
### Data Retention
Consider implementing a data retention policy to remove old partitions:
```sql
-- Drop partitions older than 3 months
DROP TABLE IF EXISTS license_heartbeats_2025_03;
```
### Troubleshooting
1. **Table already exists**: The migration is idempotent and will skip creation if the table exists.
2. **Permission denied**: Ensure the database user has CREATE privileges.
3. **Foreign key violation**: The licenses table must exist before running this migration.

Datei anzeigen

@@ -0,0 +1,58 @@
-- Migration: Add June 2025 partition for license_heartbeats table
-- This migration adds the missing partition for the current month (June 2025)
-- Check if the partition already exists before creating it
DO $$
BEGIN
-- Check if the June 2025 partition exists
IF NOT EXISTS (
SELECT 1
FROM pg_tables
WHERE tablename = 'license_heartbeats_2025_06'
) THEN
-- Create the June 2025 partition
EXECUTE 'CREATE TABLE license_heartbeats_2025_06 PARTITION OF license_heartbeats
FOR VALUES FROM (''2025-06-01'') TO (''2025-07-01'')';
RAISE NOTICE 'Created partition license_heartbeats_2025_06';
ELSE
RAISE NOTICE 'Partition license_heartbeats_2025_06 already exists';
END IF;
END $$;
-- Also create partitions for the next few months to avoid future issues
DO $$
DECLARE
partition_name text;
start_date date;
end_date date;
i integer;
BEGIN
-- Create partitions for the next 6 months
FOR i IN 0..6 LOOP
start_date := date_trunc('month', CURRENT_DATE + (i || ' months')::interval);
end_date := start_date + interval '1 month';
partition_name := 'license_heartbeats_' || to_char(start_date, 'YYYY_MM');
-- Check if partition already exists
IF NOT EXISTS (
SELECT 1
FROM pg_tables
WHERE tablename = partition_name
) THEN
EXECUTE format('CREATE TABLE %I PARTITION OF license_heartbeats FOR VALUES FROM (%L) TO (%L)',
partition_name, start_date, end_date);
RAISE NOTICE 'Created partition %', partition_name;
END IF;
END LOOP;
END $$;
-- Verify the partitions were created
SELECT
schemaname,
tablename,
tableowner
FROM pg_tables
WHERE tablename LIKE 'license_heartbeats_%'
ORDER BY tablename;

Datei anzeigen

@@ -0,0 +1,79 @@
-- Migration: Create license_heartbeats partitioned table
-- Date: 2025-06-19
-- Description: Creates the license_heartbeats table with monthly partitioning
-- Create the partitioned table
CREATE TABLE IF NOT EXISTS license_heartbeats (
id BIGSERIAL,
license_id INTEGER NOT NULL,
hardware_id VARCHAR(255) NOT NULL,
ip_address INET NOT NULL,
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
session_data JSONB,
PRIMARY KEY (id, timestamp),
FOREIGN KEY (license_id) REFERENCES licenses(id) ON DELETE CASCADE
) PARTITION BY RANGE (timestamp);
-- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_license_heartbeats_license_id_timestamp
ON license_heartbeats (license_id, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_license_heartbeats_timestamp
ON license_heartbeats (timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_license_heartbeats_hardware_id
ON license_heartbeats (hardware_id);
CREATE INDEX IF NOT EXISTS idx_license_heartbeats_ip_address
ON license_heartbeats (ip_address);
-- Create partitions for current and next month
DO $$
DECLARE
current_year INTEGER;
current_month INTEGER;
next_year INTEGER;
next_month INTEGER;
partition_name TEXT;
start_date DATE;
end_date DATE;
BEGIN
-- Get current date info
current_year := EXTRACT(YEAR FROM CURRENT_DATE);
current_month := EXTRACT(MONTH FROM CURRENT_DATE);
-- Calculate next month
IF current_month = 12 THEN
next_year := current_year + 1;
next_month := 1;
ELSE
next_year := current_year;
next_month := current_month + 1;
END IF;
-- Create current month partition
partition_name := 'license_heartbeats_' || current_year || '_' || LPAD(current_month::TEXT, 2, '0');
start_date := DATE_TRUNC('month', CURRENT_DATE);
end_date := DATE_TRUNC('month', CURRENT_DATE) + INTERVAL '1 month';
EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF license_heartbeats FOR VALUES FROM (%L) TO (%L)',
partition_name, start_date, end_date);
-- Create next month partition
partition_name := 'license_heartbeats_' || next_year || '_' || LPAD(next_month::TEXT, 2, '0');
start_date := end_date;
end_date := start_date + INTERVAL '1 month';
EXECUTE format('CREATE TABLE IF NOT EXISTS %I PARTITION OF license_heartbeats FOR VALUES FROM (%L) TO (%L)',
partition_name, start_date, end_date);
RAISE NOTICE 'Created partitions for current and next month';
END $$;
-- Add comment to the table
COMMENT ON TABLE license_heartbeats IS 'Stores heartbeat data from license validations for real-time monitoring';
COMMENT ON COLUMN license_heartbeats.license_id IS 'Foreign key to licenses table';
COMMENT ON COLUMN license_heartbeats.hardware_id IS 'Hardware identifier of the device';
COMMENT ON COLUMN license_heartbeats.ip_address IS 'IP address from which the heartbeat was sent';
COMMENT ON COLUMN license_heartbeats.timestamp IS 'Timestamp of the heartbeat';
COMMENT ON COLUMN license_heartbeats.session_data IS 'Additional session data in JSON format';

Datei anzeigen

@@ -561,91 +561,8 @@ def clear_attempts():
@admin_bp.route("/lizenzserver/monitor")
@login_required
def license_monitor():
"""License server live monitoring dashboard"""
try:
conn = get_connection()
cur = conn.cursor()
# Get current statistics
# Active validations in last 5 minutes
cur.execute("""
SELECT COUNT(DISTINCT license_id) as active_licenses,
COUNT(*) as total_validations,
COUNT(DISTINCT hardware_id) as unique_devices,
COUNT(DISTINCT ip_address) as unique_ips
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '5 minutes'
""")
live_stats = cur.fetchone()
# Get validation rate (per minute)
cur.execute("""
SELECT DATE_TRUNC('minute', timestamp) as minute,
COUNT(*) as validations
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '10 minutes'
GROUP BY minute
ORDER BY minute DESC
LIMIT 10
""")
validation_rates = cur.fetchall()
# Get top active licenses
cur.execute("""
SELECT l.id, l.license_key, c.name as customer_name,
COUNT(DISTINCT lh.hardware_id) as device_count,
COUNT(*) as validation_count,
MAX(lh.timestamp) as last_seen
FROM licenses l
JOIN customers c ON l.customer_id = c.id
JOIN license_heartbeats lh ON l.id = lh.license_id
WHERE lh.timestamp > NOW() - INTERVAL '15 minutes'
GROUP BY l.id, l.license_key, c.name
ORDER BY validation_count DESC
LIMIT 10
""")
top_licenses = cur.fetchall()
# Get recent anomalies
cur.execute("""
SELECT ad.*, l.license_key, c.name as customer_name
FROM anomaly_detections ad
LEFT JOIN licenses l ON ad.license_id = l.id
LEFT JOIN customers c ON l.customer_id = c.id
WHERE ad.resolved = false
ORDER BY ad.detected_at DESC
LIMIT 10
""")
recent_anomalies = cur.fetchall()
# Get geographic distribution
cur.execute("""
SELECT ip_address, COUNT(*) as count
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '1 hour'
AND ip_address IS NOT NULL
GROUP BY ip_address
ORDER BY count DESC
LIMIT 20
""")
geo_distribution = cur.fetchall()
return render_template('license_monitor.html',
live_stats=live_stats,
validation_rates=validation_rates,
top_licenses=top_licenses,
recent_anomalies=recent_anomalies,
geo_distribution=geo_distribution
)
except Exception as e:
flash(f'Fehler beim Laden der Monitoring-Daten: {str(e)}', 'error')
return render_template('license_monitor.html')
finally:
if 'cur' in locals():
cur.close()
if 'conn' in locals():
conn.close()
"""Redirect to new analytics page"""
return redirect(url_for('monitoring.analytics'))
@admin_bp.route("/lizenzserver/analytics")

Datei anzeigen

@@ -6,6 +6,7 @@ import os
import requests
from datetime import datetime, timedelta
import logging
from utils.partition_helper import ensure_partition_exists, check_table_exists
monitoring_bp = Blueprint('monitoring', __name__)
logger = logging.getLogger(__name__)
@@ -33,18 +34,33 @@ def login_required(f):
@monitoring_bp.route('/live-dashboard')
@login_required
def live_dashboard():
"""Live Dashboard showing active customer sessions"""
"""Live Dashboard showing active customer sessions and analytics"""
try:
conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
# Check if license_heartbeats table exists
if not check_table_exists(conn, 'license_heartbeats'):
logger.warning("license_heartbeats table does not exist")
# Return empty data
return render_template('monitoring/live_dashboard.html',
active_sessions=[],
stats={'active_licenses': 0, 'active_devices': 0, 'total_heartbeats': 0},
validation_timeline=[],
live_stats=[0, 0, 0, 0],
validation_rates=[],
recent_anomalies=[],
top_licenses=[])
# Ensure current month partition exists
ensure_partition_exists(conn, 'license_heartbeats', datetime.now())
# Get active customer sessions (last 5 minutes)
cur.execute("""
SELECT
l.id,
l.license_key,
c.company_name,
c.contact_person,
c.name as company_name,
lh.hardware_id,
lh.ip_address,
lh.timestamp as last_activity,
@@ -71,7 +87,7 @@ def live_dashboard():
""")
stats = cur.fetchone()
# Get validations per minute
# Get validations per minute (for both charts)
cur.execute("""
SELECT
DATE_TRUNC('minute', timestamp) as minute,
@@ -84,13 +100,81 @@ def live_dashboard():
""")
validation_timeline = cur.fetchall()
# Get live statistics for analytics cards
cur.execute("""
SELECT
COUNT(DISTINCT license_id) as active_licenses,
COUNT(*) as total_validations,
COUNT(DISTINCT hardware_id) as unique_devices,
COUNT(DISTINCT ip_address) as unique_ips
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '5 minutes'
""")
live_stats_data = cur.fetchone()
live_stats = [
live_stats_data['active_licenses'] or 0,
live_stats_data['total_validations'] or 0,
live_stats_data['unique_devices'] or 0,
live_stats_data['unique_ips'] or 0
]
# Get validation rates for analytics chart (last 30 minutes)
cur.execute("""
SELECT
DATE_TRUNC('minute', timestamp) as minute,
COUNT(*) as count
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '30 minutes'
GROUP BY minute
ORDER BY minute DESC
LIMIT 30
""")
validation_rates = [(row['minute'].isoformat(), row['count']) for row in cur.fetchall()]
# Get recent anomalies
cur.execute("""
SELECT
ad.*,
l.license_key,
c.name as customer_name
FROM anomaly_detections ad
LEFT JOIN licenses l ON l.id = ad.license_id
LEFT JOIN customers c ON c.id = l.customer_id
WHERE ad.detected_at > NOW() - INTERVAL '24 hours'
ORDER BY ad.detected_at DESC
LIMIT 10
""")
recent_anomalies = cur.fetchall()
# Get top active licenses
cur.execute("""
SELECT
l.license_key,
c.name as customer_name,
COUNT(DISTINCT lh.hardware_id) as device_count,
COUNT(*) as validation_count,
MAX(lh.timestamp) as last_seen
FROM license_heartbeats lh
JOIN licenses l ON l.id = lh.license_id
JOIN customers c ON c.id = l.customer_id
WHERE lh.timestamp > NOW() - INTERVAL '15 minutes'
GROUP BY l.license_key, c.name
ORDER BY validation_count DESC
LIMIT 10
""")
top_licenses = cur.fetchall()
cur.close()
conn.close()
return render_template('monitoring/live_dashboard.html',
active_sessions=active_sessions,
stats=stats,
validation_timeline=validation_timeline)
validation_timeline=validation_timeline,
live_stats=live_stats,
validation_rates=validation_rates,
recent_anomalies=recent_anomalies,
top_licenses=top_licenses)
except Exception as e:
logger.error(f"Error in live dashboard: {str(e)}")
@@ -173,7 +257,7 @@ def alerts():
SELECT
ad.*,
l.license_key,
c.company_name
c.name as company_name
FROM anomaly_detections ad
LEFT JOIN licenses l ON l.id = ad.license_id
LEFT JOIN customers c ON c.id = l.customer_id
@@ -191,9 +275,91 @@ def alerts():
@monitoring_bp.route('/analytics')
@login_required
def analytics():
"""Detailed analytics page"""
# This will integrate with the existing analytics service
return render_template('monitoring/analytics.html')
"""Combined analytics and license server status page"""
try:
conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
# Get live statistics for the top cards
cur.execute("""
SELECT
COUNT(DISTINCT license_id) as active_licenses,
COUNT(*) as total_validations,
COUNT(DISTINCT hardware_id) as unique_devices,
COUNT(DISTINCT ip_address) as unique_ips
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '5 minutes'
""")
live_stats = cur.fetchone()
live_stats = [
live_stats['active_licenses'] or 0,
live_stats['total_validations'] or 0,
live_stats['unique_devices'] or 0,
live_stats['unique_ips'] or 0
]
# Get validation rates for chart
cur.execute("""
SELECT
DATE_TRUNC('minute', timestamp) as minute,
COUNT(*) as count
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '30 minutes'
GROUP BY minute
ORDER BY minute DESC
LIMIT 30
""")
validation_rates = [(row['minute'].isoformat(), row['count']) for row in cur.fetchall()]
# Get recent anomalies
cur.execute("""
SELECT
ad.*,
l.license_key,
c.name as customer_name
FROM anomaly_detections ad
LEFT JOIN licenses l ON l.id = ad.license_id
LEFT JOIN customers c ON c.id = l.customer_id
WHERE ad.detected_at > NOW() - INTERVAL '24 hours'
ORDER BY ad.detected_at DESC
LIMIT 10
""")
recent_anomalies = cur.fetchall()
# Get top active licenses
cur.execute("""
SELECT
l.license_key,
c.name as customer_name,
COUNT(DISTINCT lh.hardware_id) as device_count,
COUNT(*) as validation_count,
MAX(lh.timestamp) as last_seen
FROM license_heartbeats lh
JOIN licenses l ON l.id = lh.license_id
JOIN customers c ON c.id = l.customer_id
WHERE lh.timestamp > NOW() - INTERVAL '15 minutes'
GROUP BY l.license_key, c.name
ORDER BY validation_count DESC
LIMIT 10
""")
top_licenses = cur.fetchall()
cur.close()
conn.close()
return render_template('monitoring/analytics.html',
live_stats=live_stats,
validation_rates=validation_rates,
recent_anomalies=recent_anomalies,
top_licenses=top_licenses)
except Exception as e:
logger.error(f"Error in analytics: {str(e)}")
return render_template('monitoring/analytics.html',
live_stats=[0, 0, 0, 0],
validation_rates=[],
recent_anomalies=[],
top_licenses=[])
# API endpoints for live data
@monitoring_bp.route('/api/live-stats')
@@ -235,7 +401,7 @@ def api_active_sessions():
cur.execute("""
SELECT
l.license_key,
c.company_name,
c.name as company_name,
lh.hardware_id,
lh.ip_address,
lh.timestamp as last_activity,

Datei anzeigen

@@ -409,24 +409,12 @@
</li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.audit_log' %}active{% endif %}" href="{{ url_for('admin.audit_log') }}">
<i class="bi bi-journal-text"></i>
<span>Audit-Log</span>
</a>
</li>
<li class="nav-item {% if request.endpoint in ['monitoring.live_dashboard', 'monitoring.system_status', 'monitoring.alerts', 'monitoring.analytics'] %}has-active-child{% endif %}">
<li class="nav-item {% if request.endpoint in ['monitoring.live_dashboard', 'monitoring.system_status', 'monitoring.alerts', 'admin.audit_log', 'admin.license_monitor', 'admin.license_analytics', 'admin.license_anomalies'] %}has-active-child{% endif %}">
<a class="nav-link has-submenu" href="{{ url_for('monitoring.live_dashboard') }}">
<i class="bi bi-activity"></i>
<span>Monitoring</span>
</a>
<ul class="sidebar-submenu">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'monitoring.live_dashboard' %}active{% endif %}" href="{{ url_for('monitoring.live_dashboard') }}">
<i class="bi bi-graph-up"></i>
<span>Live Dashboard</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'monitoring.system_status' %}active{% endif %}" href="{{ url_for('monitoring.system_status') }}">
<i class="bi bi-pc-display"></i>
@@ -434,19 +422,25 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'monitoring.alerts' %}active{% endif %}" href="{{ url_for('monitoring.alerts') }}">
<i class="bi bi-exclamation-triangle"></i>
<span>Alerts</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'monitoring.analytics' %}active{% endif %}" href="{{ url_for('monitoring.analytics') }}">
<i class="bi bi-bar-chart-line"></i>
<span>Analytics</span>
<a class="nav-link {% if request.endpoint == 'admin.license_anomalies' %}active{% endif %}" href="{{ url_for('admin.license_anomalies') }}">
<i class="bi bi-bug"></i>
<span>Lizenz-Anomalien</span>
</a>
</li>
</ul>
</li>
<li class="nav-item {% if request.endpoint in ['admin.audit_log', 'admin.backups', 'admin.blocked_ips', 'admin.license_config'] %}has-active-child{% endif %}">
<a class="nav-link has-submenu" href="{{ url_for('admin.license_config') }}">
<i class="bi bi-tools"></i>
<span>Administration</span>
</a>
<ul class="sidebar-submenu">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.audit_log' %}active{% endif %}" href="{{ url_for('admin.audit_log') }}">
<i class="bi bi-journal-text"></i>
<span>Audit-Log</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.backups' %}active{% endif %}" href="{{ url_for('admin.backups') }}">
<i class="bi bi-cloud-download"></i>
@@ -455,32 +449,8 @@
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.blocked_ips' %}active{% endif %}" href="{{ url_for('admin.blocked_ips') }}">
<i class="bi bi-shield-lock"></i>
<span>Sicherheit</span>
</a>
</li>
<li class="nav-item {% if request.endpoint in ['admin.license_monitor', 'admin.license_analytics', 'admin.license_anomalies', 'admin.license_config'] %}has-active-child{% endif %}">
<a class="nav-link has-submenu" href="{{ url_for('admin.license_monitor') }}">
<i class="bi bi-speedometer2"></i>
<span>Lizenzserver</span>
</a>
<ul class="sidebar-submenu">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.license_analytics' %}active{% endif %}" href="{{ url_for('admin.license_analytics') }}">
<i class="bi bi-bar-chart"></i>
<span>Analytics</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.license_anomalies' %}active{% endif %}" href="{{ url_for('admin.license_anomalies') }}">
<i class="bi bi-exclamation-triangle"></i>
<span>Anomalien</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'admin.license_config' %}active{% endif %}" href="{{ url_for('admin.license_config') }}">
<i class="bi bi-gear"></i>
<span>Konfiguration</span>
<i class="bi bi-slash-circle"></i>
<span>Gesperrte IPs</span>
</a>
</li>
</ul>

Datei anzeigen

@@ -94,7 +94,6 @@
</a>
</div>
<div class="col-md-4">
<a href="{{ url_for('sessions.sessions') }}" class="text-decoration-none">
<div class="card stat-card h-100">
<div class="card-body text-center">
<div class="card-icon text-success{% if stats.active_sessions > 0 %} pulse-effect{% endif %}">🟢</div>
@@ -102,7 +101,6 @@
<div class="card-label text-muted">Aktive Sessions</div>
</div>
</div>
</a>
</div>
</div>

Datei anzeigen

@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}Fehler{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center mt-5">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-danger text-white">
<h4 class="mb-0">
<i class="bi bi-exclamation-triangle-fill"></i>
{{ error_message|default('Ein Fehler ist aufgetreten') }}
</h4>
</div>
<div class="card-body">
{% if details %}
<div class="alert alert-danger">
<h6>Details:</h6>
<pre class="mb-0">{{ details }}</pre>
</div>
{% endif %}
<div class="mt-3">
<a href="{{ url_for('admin.dashboard') }}" class="btn btn-primary">
<i class="bi bi-house"></i> Zurück zum Dashboard
</a>
<button onclick="window.history.back();" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Zurück
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

Datei anzeigen

@@ -1,319 +0,0 @@
{% extends "base.html" %}
{% block title %}Lizenzserver Monitor{% endblock %}
{% block extra_css %}
<style>
.stat-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
color: var(--status-active);
}
.stat-label {
color: #6c757d;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.live-indicator {
display: inline-block;
width: 8px;
height: 8px;
background: var(--status-active);
border-radius: 50%;
animation: pulse 2s infinite;
margin-right: 5px;
}
.validation-timeline {
height: 300px;
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
}
.anomaly-alert {
padding: 1rem;
border-left: 4px solid var(--status-danger);
background: #fff5f5;
margin-bottom: 0.5rem;
}
.device-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: #e9ecef;
border-radius: 4px;
font-size: 0.85rem;
margin-right: 0.5rem;
}
</style>
{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-speedometer2"></i> Lizenzserver Live Monitor</h1>
<div>
<span class="live-indicator"></span>
<span class="text-muted">Live-Daten</span>
<button class="btn btn-sm btn-outline-secondary ms-3" onclick="toggleAutoRefresh()">
<i class="bi bi-arrow-clockwise"></i> Auto-Refresh: <span id="refresh-status">AN</span>
</button>
</div>
</div>
<!-- Live Statistics Cards -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="active-licenses">
{{ live_stats[0] if live_stats else 0 }}
</div>
<div class="stat-label">Aktive Lizenzen</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="total-validations">
{{ live_stats[1] if live_stats else 0 }}
</div>
<div class="stat-label">Validierungen (5 Min)</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="unique-devices">
{{ live_stats[2] if live_stats else 0 }}
</div>
<div class="stat-label">Aktive Geräte</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="unique-ips">
{{ live_stats[3] if live_stats else 0 }}
</div>
<div class="stat-label">Unique IPs</div>
</div>
</div>
</div>
<div class="row">
<!-- Validation Timeline -->
<div class="col-md-8 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Validierungen pro Minute</h5>
</div>
<div class="card-body">
<canvas id="validationChart" height="100"></canvas>
</div>
</div>
</div>
<!-- Recent Anomalies -->
<div class="col-md-4 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Aktuelle Anomalien</h5>
<a href="{{ url_for('admin.license_anomalies') }}" class="btn btn-sm btn-outline-primary">
Alle anzeigen
</a>
</div>
<div class="card-body" style="max-height: 400px; overflow-y: auto;">
{% if recent_anomalies %}
{% for anomaly in recent_anomalies %}
<div class="anomaly-alert">
<div class="d-flex justify-content-between">
<span class="badge badge-{{ 'danger' if anomaly['severity'] == 'critical' else anomaly['severity'] }}">
{{ anomaly['severity'].upper() }}
</span>
<small class="text-muted">{{ anomaly['detected_at'].strftime('%H:%M') }}</small>
</div>
<div class="mt-2">
<strong>{{ anomaly['anomaly_type'].replace('_', ' ').title() }}</strong><br>
<small>Lizenz: {{ anomaly['license_key'][:8] }}...</small>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted text-center">Keine aktiven Anomalien</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Top Active Licenses -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Top Aktive Lizenzen (letzte 15 Min)</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Lizenzschlüssel</th>
<th>Kunde</th>
<th>Geräte</th>
<th>Validierungen</th>
<th>Zuletzt gesehen</th>
<th>Status</th>
</tr>
</thead>
<tbody id="top-licenses-tbody">
{% for license in top_licenses %}
<tr>
<td>
<code>{{ license['license_key'][:12] }}...</code>
</td>
<td>{{ license['customer_name'] }}</td>
<td>
<span class="device-badge">
<i class="bi bi-laptop"></i> {{ license['device_count'] }}
</span>
</td>
<td>{{ license['validation_count'] }}</td>
<td>{{ license['last_seen'].strftime('%H:%M:%S') }}</td>
<td>
<span class="badge bg-success">Aktiv</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Latest Validations Stream -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">Letzte Validierungen (Live-Stream)</h5>
</div>
<div class="card-body">
<div id="validation-stream" style="max-height: 300px; overflow-y: auto;">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let autoRefresh = true;
let refreshInterval;
let validationChart;
// Initialize validation chart
const ctx = document.getElementById('validationChart').getContext('2d');
validationChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Validierungen',
data: [],
borderColor: 'rgb(40, 167, 69)',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
// Update chart with validation rates
{% if validation_rates %}
const rates = {{ validation_rates|tojson }};
validationChart.data.labels = rates.map(r => new Date(r[0]).toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'})).reverse();
validationChart.data.datasets[0].data = rates.map(r => r[1]).reverse();
validationChart.update();
{% endif %}
// Fetch live statistics
function fetchLiveStats() {
fetch('{{ url_for("admin.license_live_stats") }}')
.then(response => response.json())
.then(data => {
// Update statistics
document.getElementById('active-licenses').textContent = data.active_licenses;
document.getElementById('total-validations').textContent = data.validations_per_minute;
document.getElementById('unique-devices').textContent = data.active_devices;
// Update validation stream
const stream = document.getElementById('validation-stream');
const newEntries = data.latest_validations.map(v =>
`<div class="d-flex justify-content-between border-bottom py-2">
<span>
<code>${v.license_key}</code> |
<span class="text-muted">${v.hardware_id}</span>
</span>
<span>
<span class="badge bg-secondary">${v.ip_address}</span>
<span class="text-muted ms-2">${v.timestamp}</span>
</span>
</div>`
).join('');
if (newEntries) {
stream.innerHTML = newEntries + stream.innerHTML;
// Keep only last 20 entries
const entries = stream.querySelectorAll('div');
if (entries.length > 20) {
for (let i = 20; i < entries.length; i++) {
entries[i].remove();
}
}
}
})
.catch(error => console.error('Error fetching live stats:', error));
}
// Toggle auto-refresh
function toggleAutoRefresh() {
autoRefresh = !autoRefresh;
document.getElementById('refresh-status').textContent = autoRefresh ? 'AN' : 'AUS';
if (autoRefresh) {
refreshInterval = setInterval(fetchLiveStats, 5000);
} else {
clearInterval(refreshInterval);
}
}
// Start auto-refresh
if (autoRefresh) {
refreshInterval = setInterval(fetchLiveStats, 5000);
}
// Initial fetch
fetchLiveStats();
</script>
{% endblock %}

Datei anzeigen

@@ -1,9 +1,10 @@
{% extends "base.html" %}
{% block title %}Analytics{% endblock %}
{% block title %}Analytics & Lizenzserver Status{% endblock %}
{% block extra_css %}
<style>
/* Analytics Styles */
.analytics-card {
background: white;
border-radius: 10px;
@@ -69,14 +70,72 @@
padding: 30px;
color: #6c757d;
}
/* License Monitor Styles */
.stat-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
color: var(--status-active);
}
.live-indicator {
display: inline-block;
width: 8px;
height: 8px;
background: var(--status-active);
border-radius: 50%;
animation: pulse 2s infinite;
margin-right: 5px;
}
.validation-timeline {
height: 300px;
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
}
.anomaly-alert {
padding: 1rem;
border-left: 4px solid var(--status-danger);
background: #fff5f5;
margin-bottom: 0.5rem;
}
.device-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: #e9ecef;
border-radius: 4px;
font-size: 0.85rem;
margin-right: 0.5rem;
}
</style>
{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col">
<h2><i class="bi bi-bar-chart-line"></i> Analytics</h2>
<p class="text-muted">Detaillierte Analyse und Berichte</p>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-bar-chart-line"></i> Analytics & Lizenzserver Status</h1>
<div>
<span class="live-indicator"></span>
<span class="text-muted">Live-Daten</span>
<button class="btn btn-sm btn-outline-secondary ms-3" onclick="toggleAutoRefresh()">
<i class="bi bi-arrow-clockwise"></i> Auto-Refresh: <span id="refresh-status">AN</span>
</button>
</div>
</div>
@@ -101,40 +160,145 @@
</div>
</div>
<!-- Key Metrics Overview -->
<!-- Live Statistics Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="stat-box">
<div class="stat-value" id="active-licenses">-</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="active-licenses">
{{ live_stats[0] if live_stats else 0 }}
</div>
<div class="stat-label">Aktive Lizenzen</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-box">
<div class="stat-value" id="total-validations">-</div>
<div class="stat-label">Validierungen</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="total-validations">
{{ live_stats[1] if live_stats else 0 }}
</div>
<div class="stat-label">Validierungen (5 Min)</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-box">
<div class="stat-value" id="active-devices">-</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="unique-devices">
{{ live_stats[2] if live_stats else 0 }}
</div>
<div class="stat-label">Aktive Geräte</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="stat-card">
<div class="stat-number" id="unique-ips">
{{ live_stats[3] if live_stats else 0 }}
</div>
<div class="stat-label">Unique IPs</div>
</div>
</div>
</div>
<!-- Analytics Notice -->
<div class="analytics-card">
<div class="no-data">
<i class="bi bi-info-circle" style="font-size: 3rem; color: #6c757d;"></i>
<h5 class="mt-3">Analytics-Daten werden gesammelt</h5>
<p>Die detaillierten Analysen stehen zur Verfügung, sobald genügend Daten vorhanden sind.</p>
<p>Nutzen Sie das <a href="{{ url_for('monitoring.live_dashboard') }}">Live Dashboard</a> für Echtzeit-Statistiken.</p>
<div class="row">
<!-- Validation Timeline -->
<div class="col-md-8 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Validierungen pro Minute</h5>
</div>
<div class="card-body">
<canvas id="validationChart" height="100"></canvas>
</div>
</div>
</div>
<!-- Recent Anomalies -->
<div class="col-md-4 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Aktuelle Anomalien</h5>
<a href="{{ url_for('admin.license_anomalies') }}" class="btn btn-sm btn-outline-primary">
Alle anzeigen
</a>
</div>
<div class="card-body" style="max-height: 400px; overflow-y: auto;">
{% if recent_anomalies %}
{% for anomaly in recent_anomalies %}
<div class="anomaly-alert">
<div class="d-flex justify-content-between">
<span class="badge badge-{{ 'danger' if anomaly['severity'] == 'critical' else anomaly['severity'] }}">
{{ anomaly['severity'].upper() }}
</span>
<small class="text-muted">{{ anomaly['detected_at'].strftime('%H:%M') }}</small>
</div>
<div class="mt-2">
<strong>{{ anomaly['anomaly_type'].replace('_', ' ').title() }}</strong><br>
<small>Lizenz: {{ anomaly['license_key'][:8] }}...</small>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted text-center">Keine aktiven Anomalien</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Top Active Licenses -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Top Aktive Lizenzen (letzte 15 Min)</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Lizenzschlüssel</th>
<th>Kunde</th>
<th>Geräte</th>
<th>Validierungen</th>
<th>Zuletzt gesehen</th>
<th>Status</th>
</tr>
</thead>
<tbody id="top-licenses-tbody">
{% for license in top_licenses %}
<tr>
<td>
<code>{{ license['license_key'][:12] }}...</code>
</td>
<td>{{ license['customer_name'] }}</td>
<td>
<span class="device-badge">
<i class="bi bi-laptop"></i> {{ license['device_count'] }}
</span>
</td>
<td>{{ license['validation_count'] }}</td>
<td>{{ license['last_seen'].strftime('%H:%M:%S') }}</td>
<td>
<span class="badge bg-success">Aktiv</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Latest Validations Stream -->
<div class="card mb-4">
<div class="card-header">
<h5 class="mb-0">Letzte Validierungen (Live-Stream)</h5>
</div>
<div class="card-body">
<div id="validation-stream" style="max-height: 300px; overflow-y: auto;">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
<!-- Export Options -->
<div class="analytics-card mt-4">
<div class="analytics-card">
<h5>Berichte exportieren</h5>
<div class="export-buttons">
<button class="btn btn-outline-primary me-2" onclick="exportReport('pdf')">
@@ -151,7 +315,45 @@
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
let autoRefresh = true;
let refreshInterval;
let validationChart;
// Initialize validation chart
const ctx = document.getElementById('validationChart').getContext('2d');
validationChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Validierungen',
data: [],
borderColor: 'rgb(40, 167, 69)',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
// Update chart with validation rates
{% if validation_rates %}
const rates = {{ validation_rates|tojson }};
validationChart.data.labels = rates.map(r => new Date(r[0]).toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'})).reverse();
validationChart.data.datasets[0].data = rates.map(r => r[1]).reverse();
validationChart.update();
{% endif %}
function loadAnalyticsData() {
// Load basic statistics from database
fetch('/monitoring/api/live-stats')
@@ -159,33 +361,92 @@
.then(data => {
document.getElementById('active-licenses').textContent = data.active_licenses || '0';
document.getElementById('total-validations').textContent = data.validations_last_minute || '0';
document.getElementById('active-devices').textContent = data.active_devices || '0';
document.getElementById('unique-devices').textContent = data.active_devices || '0';
})
.catch(error => {
console.error('Error loading analytics:', error);
document.getElementById('active-licenses').textContent = '0';
document.getElementById('total-validations').textContent = '0';
document.getElementById('active-devices').textContent = '0';
document.getElementById('unique-devices').textContent = '0';
});
}
// Fetch live statistics
function fetchLiveStats() {
fetch('{{ url_for("admin.license_live_stats") }}')
.then(response => response.json())
.then(data => {
// Update statistics
document.getElementById('active-licenses').textContent = data.active_licenses;
document.getElementById('total-validations').textContent = data.validations_per_minute;
document.getElementById('unique-devices').textContent = data.active_devices;
// Update validation stream
const stream = document.getElementById('validation-stream');
const newEntries = data.latest_validations.map(v =>
`<div class="d-flex justify-content-between border-bottom py-2">
<span>
<code>${v.license_key}</code> |
<span class="text-muted">${v.hardware_id}</span>
</span>
<span>
<span class="badge bg-secondary">${v.ip_address}</span>
<span class="text-muted ms-2">${v.timestamp}</span>
</span>
</div>`
).join('');
if (newEntries) {
stream.innerHTML = newEntries + stream.innerHTML;
// Keep only last 20 entries
const entries = stream.querySelectorAll('div');
if (entries.length > 20) {
for (let i = 20; i < entries.length; i++) {
entries[i].remove();
}
}
}
})
.catch(error => console.error('Error fetching live stats:', error));
}
// Toggle auto-refresh
function toggleAutoRefresh() {
autoRefresh = !autoRefresh;
document.getElementById('refresh-status').textContent = autoRefresh ? 'AN' : 'AUS';
if (autoRefresh) {
refreshInterval = setInterval(fetchLiveStats, 5000);
} else {
clearInterval(refreshInterval);
}
}
function updateAnalytics() {
const range = document.getElementById('date-range').value;
console.log('Updating analytics for range:', range);
loadAnalyticsData();
fetchLiveStats();
}
function refreshAnalytics() {
loadAnalyticsData();
fetchLiveStats();
}
function exportReport(format) {
alert(`Export-Funktion wird implementiert für Format: ${format.toUpperCase()}`);
}
// Start auto-refresh
if (autoRefresh) {
refreshInterval = setInterval(fetchLiveStats, 5000);
}
// Load data on page load
document.addEventListener('DOMContentLoaded', function() {
loadAnalyticsData();
fetchLiveStats();
// Refresh every 30 seconds
setInterval(loadAnalyticsData, 30000);
});

Datei anzeigen

@@ -1,9 +1,10 @@
{% extends "base.html" %}
{% block title %}Live Dashboard{% endblock %}
{% block title %}Live Dashboard & Analytics{% endblock %}
{% block extra_css %}
<style>
/* Combined styles from both dashboards */
.stats-card {
background: white;
border-radius: 10px;
@@ -11,6 +12,7 @@
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.2s;
height: 100%;
}
.stats-card:hover {
@@ -83,17 +85,69 @@
display: inline-block;
margin: 2px;
}
/* Analytics specific styles */
.chart-container {
position: relative;
height: 300px;
margin-top: 20px;
}
.live-indicator {
display: inline-block;
width: 8px;
height: 8px;
background: var(--status-active);
border-radius: 50%;
animation: pulse 2s infinite;
margin-right: 5px;
}
.anomaly-alert {
padding: 1rem;
border-left: 4px solid var(--status-danger);
background: #fff5f5;
margin-bottom: 0.5rem;
}
.device-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
background: #e9ecef;
border-radius: 4px;
font-size: 0.85rem;
margin-right: 0.5rem;
}
.nav-tabs .nav-link {
color: #495057;
border: none;
border-bottom: 3px solid transparent;
padding: 0.5rem 1rem;
}
.nav-tabs .nav-link.active {
color: var(--bs-primary);
background: none;
border-bottom-color: var(--bs-primary);
}
.tab-content {
padding-top: 1.5rem;
}
</style>
{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col">
<h2><i class="bi bi-activity"></i> Live Dashboard</h2>
<p class="text-muted">Echtzeit-Übersicht der aktiven Kunden-Sessions</p>
<h2><i class="bi bi-activity"></i> Live Dashboard & Analytics</h2>
<p class="text-muted">Echtzeit-Übersicht und Analyse der Lizenznutzung</p>
</div>
<div class="col-auto">
<span class="text-muted">Auto-Refresh: <span id="refresh-countdown">30</span>s</span>
<span class="live-indicator"></span>
<span class="text-muted">Live-Daten</span>
<span class="text-muted ms-3">Auto-Refresh: <span id="refresh-countdown">30</span>s</span>
<button class="btn btn-sm btn-outline-primary ms-2" onclick="refreshData()">
<i class="bi bi-arrow-clockwise"></i> Jetzt aktualisieren
</button>
@@ -102,43 +156,153 @@
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-md-4">
<div class="col-md-3">
<div class="stats-card">
<i class="bi bi-people-fill text-primary" style="font-size: 2rem;"></i>
<div class="stats-number text-primary">{{ stats.active_licenses|default(0) }}</div>
<div class="stats-label">Aktive Kunden</div>
<div class="stats-number text-primary" id="active-licenses">{{ live_stats[0] if live_stats else 0 }}</div>
<div class="stats-label">Aktive Lizenzen</div>
</div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<div class="stats-card">
<i class="bi bi-laptop text-success" style="font-size: 2rem;"></i>
<div class="stats-number text-success">{{ stats.active_devices|default(0) }}</div>
<i class="bi bi-shield-check text-success" style="font-size: 2rem;"></i>
<div class="stats-number text-success" id="total-validations">{{ live_stats[1] if live_stats else 0 }}</div>
<div class="stats-label">Validierungen (5 Min)</div>
</div>
</div>
<div class="col-md-3">
<div class="stats-card">
<i class="bi bi-laptop text-info" style="font-size: 2rem;"></i>
<div class="stats-number text-info" id="unique-devices">{{ live_stats[2] if live_stats else 0 }}</div>
<div class="stats-label">Aktive Geräte</div>
</div>
</div>
<div class="col-md-4">
<div class="col-md-3">
<div class="stats-card">
<i class="bi bi-speedometer2 text-info" style="font-size: 2rem;"></i>
<div class="stats-number text-info" id="validations-per-minute">0</div>
<div class="stats-label">Validierungen/Min</div>
<i class="bi bi-globe text-warning" style="font-size: 2rem;"></i>
<div class="stats-number text-warning" id="unique-ips">{{ live_stats[3] if live_stats else 0 }}</div>
<div class="stats-label">Unique IPs</div>
</div>
</div>
</div>
<!-- Activity Timeline Chart -->
<div class="card mb-4">
<!-- Tabbed Interface -->
<ul class="nav nav-tabs" id="dashboardTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="overview-tab" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab">
<i class="bi bi-speedometer2"></i> Übersicht
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="sessions-tab" data-bs-toggle="tab" data-bs-target="#sessions" type="button" role="tab">
<i class="bi bi-people"></i> Aktive Sessions
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="analytics-tab" data-bs-toggle="tab" data-bs-target="#analytics" type="button" role="tab">
<i class="bi bi-bar-chart-line"></i> Analytics
</button>
</li>
</ul>
<div class="tab-content" id="dashboardTabContent">
<!-- Overview Tab -->
<div class="tab-pane fade show active" id="overview" role="tabpanel">
<div class="row">
<!-- Activity Timeline Chart -->
<div class="col-md-8 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-graph-up"></i> Aktivität (letzte 60 Minuten)</h5>
</div>
<div class="card-body">
<canvas id="activityChart" height="80"></canvas>
</div>
</div>
</div>
</div>
<!-- Active Sessions -->
<div class="card">
<!-- Recent Anomalies -->
<div class="col-md-4 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Aktuelle Anomalien</h5>
<a href="{{ url_for('admin.license_anomalies') }}" class="btn btn-sm btn-outline-primary">
Alle anzeigen
</a>
</div>
<div class="card-body" style="max-height: 400px; overflow-y: auto;">
{% if recent_anomalies %}
{% for anomaly in recent_anomalies %}
<div class="anomaly-alert">
<div class="d-flex justify-content-between">
<span class="badge bg-{{ 'danger' if anomaly['severity'] == 'critical' else anomaly['severity'] }}">
{{ anomaly['severity'].upper() }}
</span>
<small class="text-muted">{{ anomaly['detected_at'].strftime('%H:%M') }}</small>
</div>
<div class="mt-2">
<strong>{{ anomaly['anomaly_type'].replace('_', ' ').title() }}</strong><br>
<small>Lizenz: {{ anomaly['license_key'][:8] }}...</small>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted text-center">Keine aktiven Anomalien</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Top Active Licenses -->
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-people"></i> Aktive Kunden-Sessions</h5>
<h5 class="mb-0">Top Aktive Lizenzen (letzte 15 Min)</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Lizenzschlüssel</th>
<th>Kunde</th>
<th>Geräte</th>
<th>Validierungen</th>
<th>Zuletzt gesehen</th>
<th>Status</th>
</tr>
</thead>
<tbody id="top-licenses-tbody">
{% for license in top_licenses %}
<tr>
<td>
<code>{{ license['license_key'][:12] }}...</code>
</td>
<td>{{ license['customer_name'] }}</td>
<td>
<span class="device-badge">
<i class="bi bi-laptop"></i> {{ license['device_count'] }}
</span>
</td>
<td>{{ license['validation_count'] }}</td>
<td>{{ license['last_seen'].strftime('%H:%M:%S') }}</td>
<td>
<span class="badge bg-success">Aktiv</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Active Sessions Tab -->
<div class="tab-pane fade" id="sessions" role="tabpanel">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-people"></i> Aktive Kunden-Sessions (letzte 5 Minuten)</h5>
</div>
<div class="card-body">
<div id="active-sessions-container">
@@ -184,12 +348,80 @@
{% endfor %}
</div>
</div>
</div>
<!-- Latest Validations Stream -->
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0">Letzte Validierungen (Live-Stream)</h5>
</div>
<div class="card-body">
<div id="validation-stream" style="max-height: 300px; overflow-y: auto;">
<!-- Will be populated by JavaScript -->
</div>
</div>
</div>
</div>
<!-- Analytics Tab -->
<div class="tab-pane fade" id="analytics" role="tabpanel">
<div class="row">
<div class="col-md-12 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Validierungen pro Minute (30 Min)</h5>
</div>
<div class="card-body">
<canvas id="validationChart" height="100"></canvas>
</div>
</div>
</div>
</div>
<!-- Export Options -->
<div class="card">
<div class="card-header">
<h5 class="mb-0">Berichte exportieren</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<label>Zeitraum auswählen:</label>
<select class="form-select" id="date-range" onchange="updateAnalytics()">
<option value="today">Heute</option>
<option value="week" selected>Letzte 7 Tage</option>
<option value="month">Letzte 30 Tage</option>
<option value="quarter">Letztes Quartal</option>
<option value="year">Letztes Jahr</option>
</select>
</div>
<div class="col-md-6">
<label>&nbsp;</label>
<div>
<button class="btn btn-outline-primary me-2" onclick="exportReport('pdf')">
<i class="bi bi-file-pdf"></i> PDF Export
</button>
<button class="btn btn-outline-success me-2" onclick="exportReport('excel')">
<i class="bi bi-file-excel"></i> Excel Export
</button>
<button class="btn btn-outline-info" onclick="exportReport('csv')">
<i class="bi bi-file-text"></i> CSV Export
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Hidden validation timeline data for chart -->
<script id="validation-timeline-data" type="application/json">
{{ validation_timeline|tojson }}
</script>
<script id="validation-rates-data" type="application/json">
{{ validation_rates|tojson }}
</script>
{% endblock %}
{% block extra_js %}
@@ -197,6 +429,7 @@
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/index.min.js"></script>
<script>
let activityChart;
let validationChart;
let refreshInterval;
let refreshCountdown = 30;
@@ -243,6 +476,35 @@
});
}
// Initialize validation chart
function initValidationChart() {
const ctx = document.getElementById('validationChart').getContext('2d');
const ratesData = JSON.parse(document.getElementById('validation-rates-data').textContent);
validationChart = new Chart(ctx, {
type: 'line',
data: {
labels: ratesData.map(r => new Date(r[0]).toLocaleTimeString('de-DE', {hour: '2-digit', minute: '2-digit'})).reverse(),
datasets: [{
label: 'Validierungen',
data: ratesData.map(r => r[1]).reverse(),
borderColor: 'rgb(40, 167, 69)',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
tension: 0.1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// Update last activity times
function updateActivityTimes() {
document.querySelectorAll('.last-activity').forEach(el => {
@@ -270,9 +532,9 @@
const stats = await statsResponse.json();
// Update stats cards
document.querySelector('.stats-number.text-primary').textContent = stats.active_licenses || 0;
document.querySelector('.stats-number.text-success').textContent = stats.active_devices || 0;
document.getElementById('validations-per-minute').textContent = stats.validations_last_minute || 0;
document.getElementById('active-licenses').textContent = stats.active_licenses || 0;
document.getElementById('unique-devices').textContent = stats.active_devices || 0;
document.getElementById('total-validations').textContent = stats.validations_last_minute || 0;
// Get active sessions
const sessionsResponse = await fetch('/monitoring/api/active-sessions');
@@ -281,6 +543,9 @@
// Update sessions display
updateSessionsDisplay(sessions);
// Fetch live stats for validation stream
fetchLiveStats();
// Reset countdown
refreshCountdown = 30;
@@ -343,6 +608,40 @@
container.innerHTML = sessionsHtml;
}
// Fetch live statistics
function fetchLiveStats() {
fetch('{{ url_for("admin.license_live_stats") }}')
.then(response => response.json())
.then(data => {
// Update validation stream
const stream = document.getElementById('validation-stream');
const newEntries = data.latest_validations.map(v =>
`<div class="d-flex justify-content-between border-bottom py-2">
<span>
<code>${v.license_key}</code> |
<span class="text-muted">${v.hardware_id}</span>
</span>
<span>
<span class="badge bg-secondary">${v.ip_address}</span>
<span class="text-muted ms-2">${v.timestamp}</span>
</span>
</div>`
).join('');
if (newEntries) {
stream.innerHTML = newEntries + stream.innerHTML;
// Keep only last 20 entries
const entries = stream.querySelectorAll('div');
if (entries.length > 20) {
for (let i = 20; i < entries.length; i++) {
entries[i].remove();
}
}
}
})
.catch(error => console.error('Error fetching live stats:', error));
}
// Format seconds ago
function formatSecondsAgo(seconds) {
if (seconds < 60) return 'wenigen Sekunden';
@@ -354,6 +653,16 @@
return `${hours} Stunde${hours > 1 ? 'n' : ''}`;
}
function updateAnalytics() {
const range = document.getElementById('date-range').value;
console.log('Updating analytics for range:', range);
refreshData();
}
function exportReport(format) {
alert(`Export-Funktion wird implementiert für Format: ${format.toUpperCase()}`);
}
// Countdown timer
function updateCountdown() {
refreshCountdown--;
@@ -367,6 +676,7 @@
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
initActivityChart();
initValidationChart();
updateActivityTimes();
// Set up auto-refresh

Datei anzeigen

@@ -182,23 +182,6 @@
</div>
</div>
<!-- Grafana Dashboard Embed (if available) -->
{% if prometheus_data %}
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-graph-up"></i> Performance Dashboard</h5>
</div>
<div class="card-body">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
Für detaillierte Metriken besuchen Sie das
<a href="http://localhost:3000" target="_blank" class="alert-link">Grafana Dashboard</a>
</div>
<!-- Optionally embed Grafana dashboard here -->
<!-- <iframe src="http://localhost:3000/d/license-server-overview?orgId=1&theme=light" class="grafana-embed"></iframe> -->
</div>
</div>
{% endif %}
<!-- Quick Actions -->
<div class="card mt-4">

Datei anzeigen

@@ -0,0 +1,114 @@
"""
Helper functions for managing partitioned tables
"""
import psycopg2
from datetime import datetime
from dateutil.relativedelta import relativedelta
import logging
logger = logging.getLogger(__name__)
def ensure_partition_exists(conn, table_name, timestamp):
"""
Ensure a partition exists for the given timestamp.
Creates the partition if it doesn't exist.
Args:
conn: Database connection
table_name: Base table name (e.g., 'license_heartbeats')
timestamp: Timestamp to check partition for
Returns:
bool: True if partition exists or was created, False on error
"""
try:
cursor = conn.cursor()
# Get year and month from timestamp
if isinstance(timestamp, str):
timestamp = datetime.fromisoformat(timestamp)
year = timestamp.year
month = timestamp.month
# Partition name
partition_name = f"{table_name}_{year}_{month:02d}"
# Check if partition exists
cursor.execute("""
SELECT EXISTS (
SELECT 1
FROM pg_tables
WHERE tablename = %s
)
""", (partition_name,))
if cursor.fetchone()[0]:
return True
# Create partition
start_date = f"{year}-{month:02d}-01"
if month == 12:
end_date = f"{year + 1}-01-01"
else:
end_date = f"{year}-{month + 1:02d}-01"
cursor.execute(f"""
CREATE TABLE IF NOT EXISTS {partition_name} PARTITION OF {table_name}
FOR VALUES FROM ('{start_date}') TO ('{end_date}')
""")
conn.commit()
logger.info(f"Created partition {partition_name}")
cursor.close()
return True
except Exception as e:
logger.error(f"Error ensuring partition exists: {e}")
return False
def create_future_partitions(conn, table_name, months_ahead=6):
"""
Create partitions for the next N months
Args:
conn: Database connection
table_name: Base table name
months_ahead: Number of months to create partitions for
"""
current_date = datetime.now()
for i in range(months_ahead + 1):
target_date = current_date + relativedelta(months=i)
ensure_partition_exists(conn, table_name, target_date)
def check_table_exists(conn, table_name):
"""
Check if a table exists in the database
Args:
conn: Database connection
table_name: Table name to check
Returns:
bool: True if table exists, False otherwise
"""
try:
cursor = conn.cursor()
cursor.execute("""
SELECT EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_name = %s
)
""", (table_name,))
exists = cursor.fetchone()[0]
cursor.close()
return exists
except Exception as e:
logger.error(f"Error checking if table exists: {e}")
return False