diff --git a/JOURNAL.md b/JOURNAL.md index 42a7a04..4b2e7a3 100644 --- a/JOURNAL.md +++ b/JOURNAL.md @@ -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 diff --git a/LIZENZSERVER.md b/LIZENZSERVER.md index 4e52bd8..b0daaea 100644 --- a/LIZENZSERVER.md +++ b/LIZENZSERVER.md @@ -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** diff --git a/v2_adminpanel/apply_license_heartbeats_migration.py b/v2_adminpanel/apply_license_heartbeats_migration.py new file mode 100644 index 0000000..843627d --- /dev/null +++ b/v2_adminpanel/apply_license_heartbeats_migration.py @@ -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) \ No newline at end of file diff --git a/v2_adminpanel/apply_partition_migration.py b/v2_adminpanel/apply_partition_migration.py new file mode 100644 index 0000000..c4f1c5d --- /dev/null +++ b/v2_adminpanel/apply_partition_migration.py @@ -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()) \ No newline at end of file diff --git a/v2_adminpanel/docs/PARTITION_MANAGEMENT.md b/v2_adminpanel/docs/PARTITION_MANAGEMENT.md new file mode 100644 index 0000000..907b1de --- /dev/null +++ b/v2_adminpanel/docs/PARTITION_MANAGEMENT.md @@ -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'); + ``` \ No newline at end of file diff --git a/v2_adminpanel/init.sql b/v2_adminpanel/init.sql index e3569eb..fa864ca 100644 --- a/v2_adminpanel/init.sql +++ b/v2_adminpanel/init.sql @@ -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); diff --git a/v2_adminpanel/migrations/README.md b/v2_adminpanel/migrations/README.md new file mode 100644 index 0000000..b783a3a --- /dev/null +++ b/v2_adminpanel/migrations/README.md @@ -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. \ No newline at end of file diff --git a/v2_adminpanel/migrations/add_june_2025_partition.sql b/v2_adminpanel/migrations/add_june_2025_partition.sql new file mode 100644 index 0000000..4a405f4 --- /dev/null +++ b/v2_adminpanel/migrations/add_june_2025_partition.sql @@ -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; \ No newline at end of file diff --git a/v2_adminpanel/migrations/create_license_heartbeats_table.sql b/v2_adminpanel/migrations/create_license_heartbeats_table.sql new file mode 100644 index 0000000..a043122 --- /dev/null +++ b/v2_adminpanel/migrations/create_license_heartbeats_table.sql @@ -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'; \ No newline at end of file diff --git a/v2_adminpanel/routes/admin_routes.py b/v2_adminpanel/routes/admin_routes.py index 3493e39..da369e3 100644 --- a/v2_adminpanel/routes/admin_routes.py +++ b/v2_adminpanel/routes/admin_routes.py @@ -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") diff --git a/v2_adminpanel/routes/monitoring_routes.py b/v2_adminpanel/routes/monitoring_routes.py index 9bb2d42..ebc200b 100644 --- a/v2_adminpanel/routes/monitoring_routes.py +++ b/v2_adminpanel/routes/monitoring_routes.py @@ -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, diff --git a/v2_adminpanel/templates/base.html b/v2_adminpanel/templates/base.html index 3865573..fe12af9 100644 --- a/v2_adminpanel/templates/base.html +++ b/v2_adminpanel/templates/base.html @@ -409,24 +409,12 @@ - - - - -