Log page changes

This commit is contained in:
2025-08-12 21:27:59 -04:00
parent cad78e6f60
commit 2456a1e43f
5 changed files with 1289 additions and 938 deletions
+79 -88
View File
@@ -1979,57 +1979,64 @@ def admin_logs():
@app.route('/api/logs/recent') @app.route('/api/logs/recent')
@admin_required @admin_required
def api_recent_logs(): def api_recent_logs():
"""Enhanced API endpoint to get recent log entries with filtering""" """API endpoint to get recent log entries with full details"""
try: try:
# Get parameters days = request.args.get('days', 1, type=int)
days = request.args.get('days', 7, type=int)
limit = request.args.get('limit', 50, type=int) limit = request.args.get('limit', 50, type=int)
category_filter = request.args.get('category', None)
severity_filter = request.args.get('severity', None)
search_term = request.args.get('search', None)
# Verify log table exists cutoff_date = datetime.now() - timedelta(days=days)
if not logger_handler.verify_log_table_exists():
return jsonify({
'success': False,
'error': 'Log table not found or inaccessible',
'logs': [],
'total': 0
}), 500
# Get logs using the enhanced method # Enhanced SQL to get all available fields
logs = logger_handler.get_recent_logs( logs_sql = """
days=days, SELECT
limit=limit, event_id,
category_filter=category_filter, event_type,
severity_filter=severity_filter, event_category,
search_term=search_term event_description,
) event_data,
severity_level,
created_timestamp,
username,
user_id,
ip_address
FROM log_events
WHERE created_timestamp >= :cutoff_date
ORDER BY created_timestamp DESC
LIMIT :limit
"""
# Log the API access for audit purposes result = db.session.execute(text(logs_sql), {
logger_handler.log_security_event( 'cutoff_date': cutoff_date,
event_type="admin_logs_accessed", 'limit': limit
description=f"Admin {session.get('username', 'unknown')} accessed log data (days={days}, limit={limit})", }).fetchall()
severity="LOW",
additional_data={ logs = []
'filters': { for row in result:
'category': category_filter, # Parse event_data if it's JSON
'severity': severity_filter, event_data = None
'search': search_term if row.event_data:
} try:
} event_data = json.loads(row.event_data) if isinstance(row.event_data, str) else row.event_data
) except (json.JSONDecodeError, TypeError):
event_data = row.event_data
logs.append({
'event_id': row.event_id,
'event_type': row.event_type,
'event_category': row.event_category,
'description': row.event_description,
'event_data': event_data,
'severity': row.severity_level,
'timestamp': row.created_timestamp.isoformat(),
'username': row.username or 'System',
'user_id': row.user_id,
'ip_address': row.ip_address or '-'
})
return jsonify({ return jsonify({
'success': True, 'success': True,
'logs': logs, 'logs': logs,
'total': len(logs), 'total': len(logs)
'filters_applied': {
'days': days,
'category': category_filter,
'severity': severity_filter,
'search': search_term
}
}) })
except Exception as e: except Exception as e:
@@ -2037,111 +2044,95 @@ def api_recent_logs():
print(f"Error in api_recent_logs: {e}") print(f"Error in api_recent_logs: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to fetch recent logs: {str(e)}', 'error': f'Failed to fetch recent logs: {str(e)}'
'logs': [],
'total': 0
}), 500 }), 500
@app.route('/api/logs/stats') @app.route('/api/logs/stats')
@admin_required @admin_required
def api_log_stats(): def api_log_stats():
"""Enhanced API endpoint to get logging statistics""" """API endpoint to get logging statistics"""
try: try:
days = request.args.get('days', 7, type=int) days = request.args.get('days', 7, type=int)
print(f"📊 Getting log statistics for last {days} days")
# Verify log table exists # Get statistics from logger handler
if not logger_handler.verify_log_table_exists():
return jsonify({
'success': False,
'error': 'Log table not found or inaccessible',
'stats': {}
}), 500
# Get statistics
stats = logger_handler.get_log_statistics(days=days) stats = logger_handler.get_log_statistics(days=days)
print(f"📈 Retrieved stats: {stats}")
# Add some additional metadata # Ensure all expected keys exist
enhanced_stats = { expected_stats = {
'total_events': stats.get('total_events', 0), 'total_events': stats.get('total_events', 0),
'security_events': stats.get('security_events', 0), 'security_events': stats.get('security_events', 0),
'database_errors': stats.get('database_errors', 0), 'database_errors': stats.get('database_errors', 0),
'user_activities': stats.get('user_activities', 0), 'user_activities': stats.get('user_activities', 0),
'system_events': stats.get('system_events', 0), 'system_events': stats.get('system_events', 0)
'unique_users': stats.get('unique_users', 0),
'severity_breakdown': {
'high': stats.get('high_severity', 0),
'medium': stats.get('medium_severity', 0),
'low': stats.get('low_severity', 0),
'info': stats.get('info_severity', 0)
},
'category_breakdown': {
'security': stats.get('security_events', 0),
'database': stats.get('database_errors', 0),
'user_activity': stats.get('user_activities', 0),
'system': stats.get('system_events', 0)
}
} }
return jsonify({ return jsonify({
'success': True, 'success': True,
'stats': enhanced_stats, 'stats': expected_stats,
'days': days, 'days': days,
'generated_at': datetime.now().isoformat() 'timestamp': datetime.now().isoformat()
}) })
except Exception as e: except Exception as e:
logger_handler.log_database_error('api_log_stats', e) logger_handler.log_database_error('api_log_stats', e)
print(f"Error in api_log_stats: {e}") print(f"Error in api_log_stats: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to fetch log statistics: {str(e)}', 'error': f'Failed to fetch log statistics: {str(e)}',
'stats': {} 'stats': {
'total_events': 0,
'security_events': 0,
'database_errors': 0,
'user_activities': 0,
'system_events': 0
}
}), 500 }), 500
@app.route('/api/logs/cleanup', methods=['POST']) @app.route('/api/logs/cleanup', methods=['POST'])
@admin_required @admin_required
def api_cleanup_logs(): def api_cleanup_logs():
"""Enhanced API endpoint to cleanup old log entries""" """API endpoint to cleanup old log entries"""
try: try:
# Get parameters from request # Get JSON data
data = request.get_json() data = request.get_json()
if not data: if not data:
print("❌ No JSON data provided")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'No JSON data provided' 'error': 'No JSON data provided'
}), 400 }), 400
days_to_keep = data.get('days_to_keep', 90) days_to_keep = data.get('days_to_keep', 90)
print(f"🧹 Cleanup request: keep last {days_to_keep} days")
# Validate input # Validate input
if not isinstance(days_to_keep, int) or days_to_keep < 7: if not isinstance(days_to_keep, int) or days_to_keep < 7:
print(f"❌ Invalid days_to_keep: {days_to_keep}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'days_to_keep must be an integer >= 7' 'error': 'days_to_keep must be an integer >= 7'
}), 400 }), 400
if days_to_keep > 365: if days_to_keep > 365:
print(f"❌ days_to_keep too large: {days_to_keep}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': 'days_to_keep cannot exceed 365 days' 'error': 'days_to_keep cannot exceed 365 days'
}), 400 }), 400
# Verify log table exists # Perform cleanup using logger handler
if not logger_handler.verify_log_table_exists():
return jsonify({
'success': False,
'error': 'Log table not found or inaccessible'
}), 500
# Perform cleanup
deleted_count = logger_handler.cleanup_old_logs(days_to_keep=days_to_keep) deleted_count = logger_handler.cleanup_old_logs(days_to_keep=days_to_keep)
# Enhanced logging for audit trail
admin_username = session.get('username', 'unknown') admin_username = session.get('username', 'unknown')
print(f"✅ Cleanup completed by {admin_username}: {deleted_count} records deleted")
# Log the admin action
logger_handler.log_security_event( logger_handler.log_security_event(
event_type="admin_log_cleanup", event_type="admin_log_cleanup",
description=f"Admin {admin_username} performed log cleanup: {deleted_count} entries removed (keeping last {days_to_keep} days)", description=f"Admin {admin_username} performed log cleanup: {deleted_count} entries removed (keeping last {days_to_keep} days)",
severity="HIGH", # High because this is a data deletion operation severity="HIGH",
additional_data={ additional_data={
'admin_user': admin_username, 'admin_user': admin_username,
'days_to_keep': days_to_keep, 'days_to_keep': days_to_keep,
@@ -2161,7 +2152,7 @@ def api_cleanup_logs():
except Exception as e: except Exception as e:
logger_handler.log_database_error('api_cleanup_logs', e) logger_handler.log_database_error('api_cleanup_logs', e)
print(f"Error in api_cleanup_logs: {e}") print(f"Error in api_cleanup_logs: {e}")
return jsonify({ return jsonify({
'success': False, 'success': False,
'error': f'Failed to cleanup old logs: {str(e)}' 'error': f'Failed to cleanup old logs: {str(e)}'
+80 -51
View File
@@ -23,7 +23,7 @@ import logging.handlers
import json import json
import os import os
import traceback import traceback
from datetime import datetime, date from datetime import datetime, date, timedelta
from functools import wraps from functools import wraps
from flask import request, session, g from flask import request, session, g
from sqlalchemy import text from sqlalchemy import text
@@ -594,24 +594,48 @@ class AppLogger:
) )
# UTILITY METHODS # UTILITY METHODS
def get_log_statistics(self, days=7): def get_log_statistics(self, days=7):
"""Get logging statistics for the specified number of days""" """Get logging statistics for the specified number of days"""
try: try:
from datetime import datetime, timedelta # Import here as backup
cutoff_date = datetime.now() - timedelta(days=days) cutoff_date = datetime.now() - timedelta(days=days)
# Get total events # Initialize default stats
stats = {
'total_events': 0,
'security_events': 0,
'database_errors': 0,
'user_activities': 0,
'system_events': 0
}
# Check if table exists first
try:
table_check = self.db.session.execute(text("SHOW TABLES LIKE 'log_events'")).fetchone()
if not table_check:
print("⚠️ log_events table does not exist")
return stats
except Exception as table_error:
print(f"⚠️ Cannot check if log_events table exists: {table_error}")
return stats
# Get total events count
try:
total_sql = """ total_sql = """
SELECT COUNT(*) as total_events SELECT COUNT(*) as total_events
FROM log_events FROM log_events
WHERE created_timestamp >= :cutoff_date WHERE created_timestamp >= :cutoff_date
""" """
total_result = self.db.session.execute(text(total_sql), { total_result = self.db.session.execute(text(total_sql), {'cutoff_date': cutoff_date}).fetchone()
'cutoff_date': cutoff_date if total_result:
}).fetchone() stats['total_events'] = total_result.total_events
print(f"✅ Found {stats['total_events']} total events in last {days} days")
except Exception as total_error:
print(f"⚠️ Error getting total events: {total_error}")
# Get events by category # Get events by category
try:
category_sql = """ category_sql = """
SELECT SELECT
event_category, event_category,
@@ -621,36 +645,31 @@ class AppLogger:
GROUP BY event_category GROUP BY event_category
""" """
category_result = self.db.session.execute(text(category_sql), { category_result = self.db.session.execute(text(category_sql), {'cutoff_date': cutoff_date}).fetchall()
'cutoff_date': cutoff_date
}).fetchall()
# Build simple statistics dictionary (not nested)
stats = {
'total_events': total_result.total_events if total_result else 0,
'security_events': 0,
'database_errors': 0,
'user_activities': 0,
'system_events': 0
}
# Process category results
for row in category_result: for row in category_result:
if row.event_category == 'security': category = row.event_category
stats['security_events'] = row.event_count count = row.event_count
elif row.event_category == 'database': print(f"✅ Found {count} events in category: {category}")
stats['database_errors'] = row.event_count
elif row.event_category == 'user_activity':
stats['user_activities'] = row.event_count
elif row.event_category == 'system':
stats['system_events'] = row.event_count
if category == 'security':
stats['security_events'] = count
elif category == 'database':
stats['database_errors'] = count
elif category == 'user_activity':
stats['user_activities'] = count
elif category == 'system':
stats['system_events'] = count
except Exception as category_error:
print(f"⚠️ Error getting category stats: {category_error}")
print(f"📊 Final stats: {stats}")
return stats return stats
except Exception as e: except Exception as e:
print(f"❌ Error in get_log_statistics: {e}")
self.log_database_error('get_log_statistics', e) self.log_database_error('get_log_statistics', e)
print(f"Error in get_log_statistics: {e}")
# Return default stats structure
return { return {
'total_events': 0, 'total_events': 0,
'security_events': 0, 'security_events': 0,
@@ -662,9 +681,22 @@ class AppLogger:
def cleanup_old_logs(self, days_to_keep=90): def cleanup_old_logs(self, days_to_keep=90):
"""Clean up old log entries from database""" """Clean up old log entries from database"""
try: try:
from datetime import datetime, timedelta # Import here as backup
cutoff_date = datetime.now() - timedelta(days=days_to_keep) cutoff_date = datetime.now() - timedelta(days=days_to_keep)
print(f"🧹 Starting log cleanup: removing entries older than {cutoff_date}")
# Check if table exists first
try:
table_check = self.db.session.execute(text("SHOW TABLES LIKE 'log_events'")).fetchone()
if not table_check:
print("⚠️ log_events table does not exist")
return 0
except Exception as table_error:
print(f"⚠️ Cannot check if log_events table exists: {table_error}")
return 0
# First, count how many records will be deleted # First, count how many records will be deleted
try:
count_sql = """ count_sql = """
SELECT COUNT(*) as count_to_delete SELECT COUNT(*) as count_to_delete
FROM log_events FROM log_events
@@ -672,50 +704,47 @@ class AppLogger:
AND severity_level NOT IN ('ERROR', 'CRITICAL', 'HIGH') AND severity_level NOT IN ('ERROR', 'CRITICAL', 'HIGH')
""" """
count_result = self.db.session.execute(text(count_sql), { count_result = self.db.session.execute(text(count_sql), {'cutoff_date': cutoff_date}).fetchone()
'cutoff_date': cutoff_date
}).fetchone()
count_to_delete = count_result.count_to_delete if count_result else 0 count_to_delete = count_result.count_to_delete if count_result else 0
print(f"📊 Found {count_to_delete} records to delete")
if count_to_delete == 0: if count_to_delete == 0:
print("✅ No old records found to cleanup")
return 0
except Exception as count_error:
print(f"⚠️ Error counting records to delete: {count_error}")
return 0 return 0
# Perform the cleanup - exclude critical logs # Perform the cleanup - exclude critical logs
try:
cleanup_sql = """ cleanup_sql = """
DELETE FROM log_events DELETE FROM log_events
WHERE created_timestamp < :cutoff_date WHERE created_timestamp < :cutoff_date
AND severity_level NOT IN ('ERROR', 'CRITICAL', 'HIGH') AND severity_level NOT IN ('ERROR', 'CRITICAL', 'HIGH')
""" """
result = self.db.session.execute(text(cleanup_sql), { result = self.db.session.execute(text(cleanup_sql), {'cutoff_date': cutoff_date})
'cutoff_date': cutoff_date
})
deleted_count = result.rowcount deleted_count = result.rowcount
self.db.session.commit() self.db.session.commit()
# Log the cleanup operation print(f"🗑️ Successfully deleted {deleted_count} old log entries")
self.logger.info(f"Log cleanup completed: {deleted_count} entries removed (keeping entries older than {days_to_keep} days)")
# Also log to security log for audit # Log the cleanup operation
self.log_security_event( self.logger.info(f"Log cleanup completed: {deleted_count} entries removed (keeping entries newer than {days_to_keep} days)")
event_type="log_cleanup",
description=f"Admin cleaned up {deleted_count} old log entries (keeping last {days_to_keep} days)",
severity="MEDIUM",
additional_data={
'days_to_keep': days_to_keep,
'deleted_count': deleted_count,
'cutoff_date': cutoff_date.isoformat()
}
)
return deleted_count return deleted_count
except Exception as delete_error:
print(f"❌ Error during deletion: {delete_error}")
self.db.session.rollback()
return 0
except Exception as e: except Exception as e:
print(f"❌ Error in cleanup_old_logs: {e}")
self.db.session.rollback() self.db.session.rollback()
self.log_database_error('cleanup_old_logs', e) self.log_database_error('cleanup_old_logs', e)
print(f"Error in cleanup_old_logs: {e}")
return 0 return 0
def get_recent_logs(self, days=7, limit=100, category_filter=None, severity_filter=None, search_term=None): def get_recent_logs(self, days=7, limit=100, category_filter=None, severity_filter=None, search_term=None):
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+435 -102
View File
@@ -1,13 +1,12 @@
<!-- templates/admin/logs.html --> <!-- templates/admin/logs.html -->
{% extends "base_authenticated.html" %} {% extends "base_authenticated.html" %} {% block title %}System Logs - QR Code
{% block title %}System Logs - QR Code Management{% endblock %} Management{% endblock %} {% block extra_head %}
{% block extra_head %}
<!-- Admin Logs CSS --> <!-- Admin Logs CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin-logs.css') }}"> <link
{% endblock %} rel="stylesheet"
href="{{ url_for('static', filename='css/admin_logs.css') }}"
{% block content %} />
{% endblock %} {% block content %}
<div class="logs-page"> <div class="logs-page">
<!-- Page Header --> <!-- Page Header -->
<div class="logs-header"> <div class="logs-header">
@@ -22,6 +21,10 @@
<p>Monitor system activities and security events</p> <p>Monitor system activities and security events</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<button class="btn btn-secondary" onclick="testLogging()">
<i class="fas fa-flask"></i>
Test Logging
</button>
<button class="btn btn-secondary" onclick="refreshLogs()"> <button class="btn btn-secondary" onclick="refreshLogs()">
<i class="fas fa-sync-alt"></i> <i class="fas fa-sync-alt"></i>
Refresh Refresh
@@ -34,14 +37,15 @@
</div> </div>
<!-- Log Statistics --> <!-- Log Statistics -->
{% if log_stats %}
<div class="log-stats"> <div class="log-stats">
<div class="stat-card total"> <div class="stat-card total">
<div class="stat-icon"> <div class="stat-icon">
<i class="fas fa-list"></i> <i class="fas fa-list"></i>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ log_stats.total_events or 0 }}</h3> <h3 id="totalEventsCount">
{{ log_stats.total_events if log_stats else 0 }}
</h3>
<p>Total Events (7 days)</p> <p>Total Events (7 days)</p>
</div> </div>
</div> </div>
@@ -51,7 +55,9 @@
<i class="fas fa-shield-alt"></i> <i class="fas fa-shield-alt"></i>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ log_stats.security_events or 0 }}</h3> <h3 id="securityEventsCount">
{{ log_stats.security_events if log_stats else 0 }}
</h3>
<p>Security Events</p> <p>Security Events</p>
</div> </div>
</div> </div>
@@ -61,7 +67,9 @@
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ log_stats.database_errors or 0 }}</h3> <h3 id="databaseErrorsCount">
{{ log_stats.database_errors if log_stats else 0 }}
</h3>
<p>Database Errors</p> <p>Database Errors</p>
</div> </div>
</div> </div>
@@ -71,11 +79,36 @@
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ log_stats.user_activities or 0 }}</h3> <h3 id="userActivitiesCount">
{{ log_stats.user_activities if log_stats else 0 }}
</h3>
<p>User Activities</p> <p>User Activities</p>
</div> </div>
</div> </div>
</div> </div>
<!-- Debug Info (only visible in development) -->
{% if config.DEBUG %}
<div
class="debug-info"
style="
background: #f3f4f6;
padding: 1rem;
border-radius: 8px;
margin-bottom: 2rem;
font-family: monospace;
font-size: 0.875rem;
"
>
<strong>Debug Info (Development Mode):</strong><br />
Log Stats Available: {{ log_stats is not none }}<br />
{% if log_stats %} Log Stats Keys: {{ log_stats.keys() | list }}<br />
Total Events: {{ log_stats.total_events }}<br />
Security Events: {{ log_stats.security_events }}<br />
Database Errors: {{ log_stats.database_errors }}<br />
User Activities: {{ log_stats.user_activities }}<br />
{% endif %}
</div>
{% endif %} {% endif %}
<!-- Log Controls --> <!-- Log Controls -->
@@ -132,13 +165,13 @@
<p>Loading log entries...</p> <p>Loading log entries...</p>
</div> </div>
<div class="empty-state" id="emptyState" style="display: none;"> <div class="empty-state" id="emptyState" style="display: none">
<i class="fas fa-clipboard-list"></i> <i class="fas fa-clipboard-list"></i>
<h3>No Log Entries Found</h3> <h3>No Log Entries Found</h3>
<p>No log entries match your current filter criteria.</p> <p>No log entries match your current filter criteria.</p>
</div> </div>
<table class="logs-table" id="logsTable" style="display: none;"> <table class="logs-table" id="logsTable" style="display: none">
<thead> <thead>
<tr> <tr>
<th>Timestamp</th> <th>Timestamp</th>
@@ -148,6 +181,7 @@
<th>Severity</th> <th>Severity</th>
<th>User</th> <th>User</th>
<th>IP Address</th> <th>IP Address</th>
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="logsTableBody"> <tbody id="logsTableBody">
@@ -158,7 +192,7 @@
</div> </div>
<!-- Pagination --> <!-- Pagination -->
<div class="pagination-wrapper" id="paginationWrapper" style="display: none;"> <div class="pagination-wrapper" id="paginationWrapper" style="display: none">
<div class="pagination-info"> <div class="pagination-info">
<span id="paginationInfo">Showing 0 of 0 entries</span> <span id="paginationInfo">Showing 0 of 0 entries</span>
</div> </div>
@@ -178,13 +212,93 @@
<!-- Cleanup Modal --> <!-- Cleanup Modal -->
<div class="modal" id="cleanupModal"> <div class="modal" id="cleanupModal">
<div class="modal" id="logDetailsModal">
<div class="modal-content log-details-modal">
<div class="modal-header">
<h3><i class="fas fa-info-circle"></i> Log Entry Details</h3>
<button class="modal-close" onclick="closeLogDetailsModal()">
&times;
</button>
</div>
<div class="modal-body">
<div class="log-details-grid">
<div class="detail-section">
<h4><i class="fas fa-tag"></i> Event Information</h4>
<div class="detail-row">
<span class="detail-label">Event ID:</span>
<span class="detail-value" id="detailEventId">-</span>
</div>
<div class="detail-row">
<span class="detail-label">Event Type:</span>
<span class="detail-value" id="detailEventType">-</span>
</div>
<div class="detail-row">
<span class="detail-label">Category:</span>
<span class="detail-value" id="detailCategory">-</span>
</div>
<div class="detail-row">
<span class="detail-label">Severity:</span>
<span class="detail-value" id="detailSeverity">-</span>
</div>
<div class="detail-row">
<span class="detail-label">Timestamp:</span>
<span class="detail-value" id="detailTimestamp">-</span>
</div>
</div>
<div class="detail-section">
<h4><i class="fas fa-user"></i> User Information</h4>
<div class="detail-row">
<span class="detail-label">Username:</span>
<span class="detail-value" id="detailUsername">-</span>
</div>
<div class="detail-row">
<span class="detail-label">User ID:</span>
<span class="detail-value" id="detailUserId">-</span>
</div>
<div class="detail-row">
<span class="detail-label">IP Address:</span>
<span class="detail-value" id="detailIpAddress">-</span>
</div>
</div>
</div>
<div class="detail-section full-width">
<h4><i class="fas fa-align-left"></i> Description</h4>
<div class="description-box" id="detailDescription">-</div>
</div>
<div
class="detail-section full-width"
id="eventDataSection"
style="display: none"
>
<h4><i class="fas fa-code"></i> Additional Data</h4>
<div class="json-box" id="detailEventData">-</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="copyLogDetails()">
<i class="fas fa-copy"></i>
Copy Details
</button>
<button class="btn btn-secondary" onclick="closeLogDetailsModal()">
<i class="fas fa-times"></i>
Close
</button>
</div>
</div>
</div>
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h3><i class="fas fa-trash-alt"></i> Cleanup Old Logs</h3> <h3><i class="fas fa-trash-alt"></i> Cleanup Old Logs</h3>
<button class="modal-close" onclick="closeCleanupModal()">&times;</button> <button class="modal-close" onclick="closeCleanupModal()">&times;</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p>This action will permanently delete log entries older than the specified number of days.</p> <p>
This action will permanently delete log entries older than the specified
number of days.
</p>
<div class="form-group"> <div class="form-group">
<label for="daysToKeep">Keep logs for the last:</label> <label for="daysToKeep">Keep logs for the last:</label>
<select id="daysToKeep" class="form-control"> <select id="daysToKeep" class="form-control">
@@ -197,11 +311,14 @@
</div> </div>
<div class="warning-note"> <div class="warning-note">
<i class="fas fa-exclamation-triangle"></i> <i class="fas fa-exclamation-triangle"></i>
<strong>Warning:</strong> This action cannot be undone. Deleted log entries will be permanently removed from the system. <strong>Warning:</strong> This action cannot be undone. Deleted log
entries will be permanently removed from the system.
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn btn-secondary" onclick="closeCleanupModal()">Cancel</button> <button class="btn btn-secondary" onclick="closeCleanupModal()">
Cancel
</button>
<button class="btn btn-danger" onclick="confirmCleanup()"> <button class="btn btn-danger" onclick="confirmCleanup()">
<i class="fas fa-trash-alt"></i> <i class="fas fa-trash-alt"></i>
Delete Old Logs Delete Old Logs
@@ -216,14 +333,15 @@ let currentPage = 1;
let logsPerPage = 50; let logsPerPage = 50;
let totalLogs = 0; let totalLogs = 0;
let currentFilters = { let currentFilters = {
search: '', search: "",
category: '', category: "",
severity: '', severity: "",
days: 7 days: 7,
}; };
// Initialize page // Initialize page
document.addEventListener('DOMContentLoaded', function() { document.addEventListener("DOMContentLoaded", function () {
console.log("Admin logs page loading...");
loadLogs(); loadLogs();
setupEventListeners(); setupEventListeners();
}); });
@@ -231,9 +349,10 @@ document.addEventListener('DOMContentLoaded', function() {
// Setup event listeners // Setup event listeners
function setupEventListeners() { function setupEventListeners() {
// Search input // Search input
const searchInput = document.getElementById('searchLogs'); const searchInput = document.getElementById("searchLogs");
if (searchInput) {
let searchTimeout; let searchTimeout;
searchInput.addEventListener('input', function() { searchInput.addEventListener("input", function () {
clearTimeout(searchTimeout); clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
currentFilters.search = this.value; currentFilters.search = this.value;
@@ -241,40 +360,56 @@ function setupEventListeners() {
loadLogs(); loadLogs();
}, 500); }, 500);
}); });
}
// Filter selects // Filter selects
document.getElementById('categoryFilter').addEventListener('change', function() { const categoryFilter = document.getElementById("categoryFilter");
if (categoryFilter) {
categoryFilter.addEventListener("change", function () {
currentFilters.category = this.value; currentFilters.category = this.value;
currentPage = 1; currentPage = 1;
loadLogs(); loadLogs();
}); });
}
document.getElementById('severityFilter').addEventListener('change', function() { const severityFilter = document.getElementById("severityFilter");
if (severityFilter) {
severityFilter.addEventListener("change", function () {
currentFilters.severity = this.value; currentFilters.severity = this.value;
currentPage = 1; currentPage = 1;
loadLogs(); loadLogs();
}); });
}
document.getElementById('daysFilter').addEventListener('change', function() { const daysFilter = document.getElementById("daysFilter");
if (daysFilter) {
daysFilter.addEventListener("change", function () {
currentFilters.days = parseInt(this.value); currentFilters.days = parseInt(this.value);
currentPage = 1; currentPage = 1;
loadLogs(); loadLogs();
loadStats(); loadStats();
}); });
} }
}
// Load logs from API // Load logs from API
async function loadLogs() { async function loadLogs() {
console.log("Loading logs...");
showLoading(); showLoading();
try { try {
const params = new URLSearchParams({ const params = new URLSearchParams({
days: currentFilters.days, days: currentFilters.days,
limit: logsPerPage, limit: logsPerPage,
page: currentPage page: currentPage,
}); });
const response = await fetch(`/api/logs/recent?${params}`); const response = await fetch(`/api/logs/recent?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
@@ -282,32 +417,36 @@ async function loadLogs() {
totalLogs = data.total || data.logs.length; totalLogs = data.total || data.logs.length;
updatePagination(); updatePagination();
} else { } else {
showError('Failed to load logs: ' + (data.error || 'Unknown error')); showError("Failed to load logs: " + (data.error || "Unknown error"));
} }
} catch (error) { } catch (error) {
console.error('Error loading logs:', error); console.error("Error loading logs:", error);
showError('Failed to load logs. Please try again.'); showError("Failed to load logs: " + error.message);
} }
} }
// Load log statistics // Load log statistics
async function loadStats() { async function loadStats() {
try { try {
const response = await fetch(`/api/logs/stats?days=${currentFilters.days}`); const response = await fetch(
`/api/logs/stats?days=${currentFilters.days}`
);
const data = await response.json(); const data = await response.json();
if (data.success && data.stats) { if (data.success && data.stats) {
updateStatsDisplay(data.stats); updateStatsDisplay(data.stats);
} else {
console.error("Failed to load stats:", data.error);
} }
} catch (error) { } catch (error) {
console.error('Error loading stats:', error); console.error("Error loading stats:", error);
} }
} }
// Display logs in table // Display logs in table
function displayLogs(logs) { function displayLogs(logs) {
const tbody = document.getElementById('logsTableBody'); const tbody = document.getElementById("logsTableBody");
tbody.innerHTML = ''; tbody.innerHTML = "";
if (logs.length === 0) { if (logs.length === 0) {
showEmpty(); showEmpty();
@@ -315,17 +454,22 @@ function displayLogs(logs) {
} }
// Filter logs based on current filters // Filter logs based on current filters
let filteredLogs = logs.filter(log => { let filteredLogs = logs.filter((log) => {
if (currentFilters.search) { if (currentFilters.search) {
const searchLower = currentFilters.search.toLowerCase(); const searchLower = currentFilters.search.toLowerCase();
if (!log.event_type.toLowerCase().includes(searchLower) && if (
!log.event_type.toLowerCase().includes(searchLower) &&
!log.description.toLowerCase().includes(searchLower) && !log.description.toLowerCase().includes(searchLower) &&
!(log.username && log.username.toLowerCase().includes(searchLower))) { !(log.username && log.username.toLowerCase().includes(searchLower))
) {
return false; return false;
} }
} }
if (currentFilters.category && log.event_category !== currentFilters.category) { if (
currentFilters.category &&
log.event_category !== currentFilters.category
) {
return false; return false;
} }
@@ -336,88 +480,244 @@ function displayLogs(logs) {
return true; return true;
}); });
filteredLogs.forEach(log => { filteredLogs.forEach((log, index) => {
const row = document.createElement('tr'); const row = document.createElement("tr");
row.className = getSeverityClass(log.severity); row.className = getSeverityClass(log.severity);
const timestamp = new Date(log.timestamp).toLocaleString(); const timestamp = new Date(log.timestamp).toLocaleString();
// Truncate description for table display
const shortDescription =
log.description && log.description.length > 80
? log.description.substring(0, 80) + "..."
: log.description || "No description";
row.innerHTML = ` row.innerHTML = `
<td class="timestamp">${timestamp}</td> <td class="timestamp">${timestamp}</td>
<td class="event-type">${escapeHtml(log.event_type)}</td> <td class="event-type">${escapeHtml(log.event_type || "Unknown")}</td>
<td class="category"> <td class="category">
<span class="category-badge ${log.event_category}">${log.event_category}</span> <span class="category-badge ${log.event_category || "system"}">${
log.event_category || "system"
}</span>
</td> </td>
<td class="description">${escapeHtml(log.description)}</td> <td class="description" title="${escapeHtml(
log.description || ""
)}">${escapeHtml(shortDescription)}</td>
<td class="severity"> <td class="severity">
<span class="severity-badge ${log.severity.toLowerCase()}">${log.severity}</span> <span class="severity-badge ${(
log.severity || "info"
).toLowerCase()}">${log.severity || "INFO"}</span>
</td>
<td class="username">${
log.username ? escapeHtml(log.username) : "<em>System</em>"
}</td>
<td class="ip-address">${log.ip_address || "-"}</td>
<td class="actions">
<button class="btn btn-sm btn-info" onclick="viewLogDetails(${index})" title="View Details">
<i class="fas fa-eye"></i>
</button>
</td> </td>
<td class="username">${log.username ? escapeHtml(log.username) : '<em>System</em>'}</td>
<td class="ip-address">${log.ip_address || '-'}</td>
`; `;
tbody.appendChild(row); tbody.appendChild(row);
}); });
// Store filtered logs globally for details modal
window.currentLogs = filteredLogs;
showTable(); showTable();
} }
// View log details in modal
function viewLogDetails(logIndex) {
const log = window.currentLogs[logIndex];
if (!log) return;
// Populate modal with log details
document.getElementById("detailEventId").textContent = log.event_id || "-";
document.getElementById("detailEventType").textContent =
log.event_type || "-";
document.getElementById("detailCategory").textContent =
log.event_category || "-";
document.getElementById(
"detailSeverity"
).innerHTML = `<span class="severity-badge ${(
log.severity || "info"
).toLowerCase()}">${log.severity || "INFO"}</span>`;
document.getElementById("detailTimestamp").textContent = new Date(
log.timestamp
).toLocaleString();
document.getElementById("detailUsername").textContent =
log.username || "System";
document.getElementById("detailUserId").textContent = log.user_id || "-";
document.getElementById("detailIpAddress").textContent =
log.ip_address || "-";
document.getElementById("detailDescription").textContent =
log.description || "-";
// Show additional data if available
const eventDataSection = document.getElementById("eventDataSection");
const eventDataElement = document.getElementById("detailEventData");
if (log.event_data) {
try {
const formattedData =
typeof log.event_data === "string"
? JSON.stringify(JSON.parse(log.event_data), null, 2)
: JSON.stringify(log.event_data, null, 2);
eventDataElement.textContent = formattedData;
eventDataSection.style.display = "block";
} catch (e) {
eventDataElement.textContent = log.event_data;
eventDataSection.style.display = "block";
}
} else {
eventDataSection.style.display = "none";
}
// Store current log for copying
window.currentLogDetails = log;
// Show modal
document.getElementById("logDetailsModal").style.display = "flex";
}
// Close log details modal
function closeLogDetailsModal() {
document.getElementById("logDetailsModal").style.display = "none";
}
// Copy log details to clipboard
function copyLogDetails() {
const log = window.currentLogDetails;
if (!log) return;
const details = `Log Entry Details
==================
Event ID: ${log.event_id || "-"}
Event Type: ${log.event_type || "-"}
Category: ${log.event_category || "-"}
Severity: ${log.severity || "-"}
Timestamp: ${new Date(log.timestamp).toLocaleString()}
Username: ${log.username || "System"}
User ID: ${log.user_id || "-"}
IP Address: ${log.ip_address || "-"}
Description:
${log.description || "-"}
${
log.event_data
? "Additional Data:\n" +
(typeof log.event_data === "string"
? log.event_data
: JSON.stringify(log.event_data, null, 2))
: ""
}`;
navigator.clipboard
.writeText(details)
.then(() => {
showSuccess("Log details copied to clipboard!");
})
.catch(() => {
// Fallback for older browsers
const textArea = document.createElement("textarea");
textArea.value = details;
document.body.appendChild(textArea);
textArea.select();
document.execCommand("copy");
document.body.removeChild(textArea);
showSuccess("Log details copied to clipboard!");
});
}
// Utility functions // Utility functions
function getSeverityClass(severity) { function getSeverityClass(severity) {
switch (severity) { switch ((severity || "info").toLowerCase()) {
case 'HIGH': return 'severity-high'; case "high":
case 'MEDIUM': return 'severity-medium'; return "severity-high";
case 'LOW': return 'severity-low'; case "medium":
default: return 'severity-info'; return "severity-medium";
case "low":
return "severity-low";
default:
return "severity-info";
} }
} }
function escapeHtml(text) { function escapeHtml(text) {
if (!text) return ''; if (!text) return "";
const div = document.createElement('div'); const div = document.createElement("div");
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
} }
function showLoading() { function showLoading() {
document.getElementById('loadingState').style.display = 'block'; document.getElementById("loadingState").style.display = "block";
document.getElementById('emptyState').style.display = 'none'; document.getElementById("emptyState").style.display = "none";
document.getElementById('logsTable').style.display = 'none'; document.getElementById("logsTable").style.display = "none";
document.getElementById('paginationWrapper').style.display = 'none'; document.getElementById("paginationWrapper").style.display = "none";
} }
function showEmpty() { function showEmpty() {
document.getElementById('loadingState').style.display = 'none'; document.getElementById("loadingState").style.display = "none";
document.getElementById('emptyState').style.display = 'block'; document.getElementById("emptyState").style.display = "block";
document.getElementById('logsTable').style.display = 'none'; document.getElementById("logsTable").style.display = "none";
document.getElementById('paginationWrapper').style.display = 'none'; document.getElementById("paginationWrapper").style.display = "none";
} }
function showTable() { function showTable() {
document.getElementById('loadingState').style.display = 'none'; document.getElementById("loadingState").style.display = "none";
document.getElementById('emptyState').style.display = 'none'; document.getElementById("emptyState").style.display = "none";
document.getElementById('logsTable').style.display = 'table'; document.getElementById("logsTable").style.display = "table";
document.getElementById('paginationWrapper').style.display = 'flex'; document.getElementById("paginationWrapper").style.display = "flex";
} }
function showError(message) { function showError(message) {
// Simple error display - you could enhance this with a proper toast/notification system showMessage("❌ " + message, "error");
alert(message);
showEmpty(); showEmpty();
} }
function showSuccess(message) {
showMessage("✅ " + message, "success");
}
function showMessage(message, type) {
const messageDiv = document.createElement("div");
messageDiv.className = `flash-message flash-${type}`;
messageDiv.innerHTML = `
<span>${message}</span>
<button onclick="this.parentElement.remove()" style="background: none; border: none; color: inherit; font-size: 1.2em; cursor: pointer; margin-left: 10px;">&times;</button>
`;
const container = document.querySelector(".logs-page");
if (container) {
container.insertBefore(messageDiv, container.firstChild);
}
setTimeout(() => {
if (messageDiv.parentElement) {
messageDiv.remove();
}
}, 5000);
}
// Update pagination // Update pagination
function updatePagination() { function updatePagination() {
const totalPages = Math.ceil(totalLogs / logsPerPage); const totalPages = Math.ceil(totalLogs / logsPerPage);
const info = document.getElementById('paginationInfo'); const info = document.getElementById("paginationInfo");
if (info) {
const start = (currentPage - 1) * logsPerPage + 1; const start = (currentPage - 1) * logsPerPage + 1;
const end = Math.min(currentPage * logsPerPage, totalLogs); const end = Math.min(currentPage * logsPerPage, totalLogs);
info.textContent = `Showing ${start}-${end} of ${totalLogs} entries`; info.textContent = `Showing ${start}-${end} of ${totalLogs} entries`;
}
document.getElementById('prevPage').disabled = currentPage <= 1; const prevBtn = document.getElementById("prevPage");
document.getElementById('nextPage').disabled = currentPage >= totalPages; const nextBtn = document.getElementById("nextPage");
if (prevBtn) prevBtn.disabled = currentPage <= 1;
if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
} }
// Pagination controls // Pagination controls
@@ -439,64 +739,92 @@ function refreshLogs() {
loadStats(); loadStats();
} }
// Export logs // Test logging functionality
function exportLogs() { async function testLogging() {
const params = new URLSearchParams({ try {
days: currentFilters.days, showMessage("Testing logging system...", "info");
format: 'csv'
});
window.open(`/api/logs/export?${params}`, '_blank'); const response = await fetch("/api/logs/test");
const data = await response.json();
if (data.success) {
showMessage("✅ Logging system test passed!", "success");
setTimeout(() => {
loadLogs();
loadStats();
}, 1000);
} else {
showError(
"❌ Logging system test failed: " + (data.error || "Unknown error")
);
}
} catch (error) {
console.error("Error testing logging:", error);
showError("❌ Failed to run logging test: " + error.message);
}
} }
// Cleanup modal functions // Cleanup logs
function cleanupLogs() { function cleanupLogs() {
document.getElementById('cleanupModal').style.display = 'flex'; document.getElementById("cleanupModal").style.display = "flex";
} }
function closeCleanupModal() { function closeCleanupModal() {
document.getElementById('cleanupModal').style.display = 'none'; document.getElementById("cleanupModal").style.display = "none";
} }
async function confirmCleanup() { async function confirmCleanup() {
const daysToKeep = parseInt(document.getElementById('daysToKeep').value); const daysToKeep = parseInt(document.getElementById("daysToKeep").value);
if (daysToKeep < 7) {
showError("Cannot keep logs for less than 7 days");
return;
}
try { try {
const response = await fetch('/api/logs/cleanup', { showMessage("Cleaning up old logs...", "info");
method: 'POST',
const response = await fetch("/api/logs/cleanup", {
method: "POST",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({ days_to_keep: daysToKeep }) body: JSON.stringify({ days_to_keep: daysToKeep }),
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
alert(`Successfully cleaned up ${data.deleted_count} old log entries.`); showSuccess(
`Successfully cleaned up ${data.deleted_count} old log entries`
);
closeCleanupModal(); closeCleanupModal();
setTimeout(() => {
loadLogs(); loadLogs();
loadStats(); loadStats();
}, 1000);
} else { } else {
alert('Error cleaning up logs: ' + (data.error || 'Unknown error')); showError("Error cleaning up logs: " + (data.error || "Unknown error"));
} }
} catch (error) { } catch (error) {
console.error('Error cleaning up logs:', error); console.error("Error cleaning up logs:", error);
alert('Failed to cleanup logs. Please try again.'); showError("Failed to cleanup logs. Please try again.");
} }
} }
function updateStatsDisplay(stats) { function updateStatsDisplay(stats) {
// Update stat cards if they exist const totalElement = document.getElementById("totalEventsCount");
const totalElement = document.querySelector('.stat-card.total h3'); const securityElement = document.getElementById("securityEventsCount");
const securityElement = document.querySelector('.stat-card.security h3'); const errorsElement = document.getElementById("databaseErrorsCount");
const errorsElement = document.querySelector('.stat-card.errors h3'); const usersElement = document.getElementById("userActivitiesCount");
const usersElement = document.querySelector('.stat-card.users h3');
if (totalElement) totalElement.textContent = stats.total_events || 0; if (totalElement) totalElement.textContent = stats.total_events || 0;
if (securityElement) securityElement.textContent = stats.security_events || 0; if (securityElement)
securityElement.textContent = stats.security_events || 0;
if (errorsElement) errorsElement.textContent = stats.database_errors || 0; if (errorsElement) errorsElement.textContent = stats.database_errors || 0;
if (usersElement) usersElement.textContent = stats.user_activities || 0; if (usersElement) usersElement.textContent = stats.user_activities || 0;
console.log("Stats updated:", stats);
} }
</script> </script>
@@ -504,7 +832,7 @@ function updateStatsDisplay(stats) {
/* Admin Logs Styles */ /* Admin Logs Styles */
.logs-page { .logs-page {
padding: var(--spacing-6); padding: var(--spacing-6);
max-width: 1400px; max-width: 1600px;
margin: 0 auto; margin: 0 auto;
} }
@@ -585,7 +913,11 @@ function updateStatsDisplay(stats) {
justify-content: center; justify-content: center;
font-size: var(--font-size-xl); font-size: var(--font-size-xl);
color: var(--white); color: var(--white);
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover)); background: linear-gradient(
135deg,
var(--primary-color),
var(--primary-hover)
);
} }
.stat-icon.security { .stat-icon.security {
@@ -639,7 +971,8 @@ function updateStatsDisplay(stats) {
.search-box input { .search-box input {
width: 100%; width: 100%;
padding: var(--spacing-3) var(--spacing-3) var(--spacing-3) var(--spacing-10); padding: var(--spacing-3) var(--spacing-3) var(--spacing-3)
var(--spacing-10);
border: 1px solid var(--gray-300); border: 1px solid var(--gray-300);
border-radius: var(--radius); border-radius: var(--radius);
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
+2 -4
View File
@@ -24,7 +24,7 @@
<link <link
rel="icon" rel="icon"
type="image/x-icon" type="image/x-icon"
href="{{ url_for('static', filename='favicon.ico') }}" href="{{ url_for('static', filename='images/favicon.ico') }}"
/> />
</head> </head>
<body class="login-layout"> <body class="login-layout">
@@ -52,9 +52,7 @@
{% endif %} {% endwith %} {% endif %} {% endwith %}
<!-- Page Content --> <!-- Page Content -->
<div class="container"> <div class="container">{% block content %}{% endblock %}</div>
{% block content %}{% endblock %}
</div>
</main> </main>
<!-- Footer --> <!-- Footer -->