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')
@admin_required
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:
# Get parameters
days = request.args.get('days', 7, type=int)
days = request.args.get('days', 1, 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
if not logger_handler.verify_log_table_exists():
return jsonify({
'success': False,
'error': 'Log table not found or inaccessible',
'logs': [],
'total': 0
}), 500
cutoff_date = datetime.now() - timedelta(days=days)
# Get logs using the enhanced method
logs = logger_handler.get_recent_logs(
days=days,
limit=limit,
category_filter=category_filter,
severity_filter=severity_filter,
search_term=search_term
)
# Enhanced SQL to get all available fields
logs_sql = """
SELECT
event_id,
event_type,
event_category,
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
logger_handler.log_security_event(
event_type="admin_logs_accessed",
description=f"Admin {session.get('username', 'unknown')} accessed log data (days={days}, limit={limit})",
severity="LOW",
additional_data={
'filters': {
'category': category_filter,
'severity': severity_filter,
'search': search_term
}
}
)
result = db.session.execute(text(logs_sql), {
'cutoff_date': cutoff_date,
'limit': limit
}).fetchall()
logs = []
for row in result:
# Parse event_data if it's JSON
event_data = None
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({
'success': True,
'logs': logs,
'total': len(logs),
'filters_applied': {
'days': days,
'category': category_filter,
'severity': severity_filter,
'search': search_term
}
'total': len(logs)
})
except Exception as e:
@@ -2037,111 +2044,95 @@ def api_recent_logs():
print(f"Error in api_recent_logs: {e}")
return jsonify({
'success': False,
'error': f'Failed to fetch recent logs: {str(e)}',
'logs': [],
'total': 0
'error': f'Failed to fetch recent logs: {str(e)}'
}), 500
@app.route('/api/logs/stats')
@admin_required
def api_log_stats():
"""Enhanced API endpoint to get logging statistics"""
"""API endpoint to get logging statistics"""
try:
days = request.args.get('days', 7, type=int)
print(f"📊 Getting log statistics for last {days} days")
# Verify log table exists
if not logger_handler.verify_log_table_exists():
return jsonify({
'success': False,
'error': 'Log table not found or inaccessible',
'stats': {}
}), 500
# Get statistics
# Get statistics from logger handler
stats = logger_handler.get_log_statistics(days=days)
print(f"📈 Retrieved stats: {stats}")
# Add some additional metadata
enhanced_stats = {
# Ensure all expected keys exist
expected_stats = {
'total_events': stats.get('total_events', 0),
'security_events': stats.get('security_events', 0),
'database_errors': stats.get('database_errors', 0),
'user_activities': stats.get('user_activities', 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)
}
'system_events': stats.get('system_events', 0)
}
return jsonify({
'success': True,
'stats': enhanced_stats,
'stats': expected_stats,
'days': days,
'generated_at': datetime.now().isoformat()
'timestamp': datetime.now().isoformat()
})
except Exception as 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({
'success': False,
'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
@app.route('/api/logs/cleanup', methods=['POST'])
@admin_required
def api_cleanup_logs():
"""Enhanced API endpoint to cleanup old log entries"""
"""API endpoint to cleanup old log entries"""
try:
# Get parameters from request
# Get JSON data
data = request.get_json()
if not data:
print("❌ No JSON data provided")
return jsonify({
'success': False,
'error': 'No JSON data provided'
}), 400
days_to_keep = data.get('days_to_keep', 90)
print(f"🧹 Cleanup request: keep last {days_to_keep} days")
# Validate input
if not isinstance(days_to_keep, int) or days_to_keep < 7:
print(f"❌ Invalid days_to_keep: {days_to_keep}")
return jsonify({
'success': False,
'error': 'days_to_keep must be an integer >= 7'
}), 400
if days_to_keep > 365:
print(f"❌ days_to_keep too large: {days_to_keep}")
return jsonify({
'success': False,
'error': 'days_to_keep cannot exceed 365 days'
}), 400
# Verify log table exists
if not logger_handler.verify_log_table_exists():
return jsonify({
'success': False,
'error': 'Log table not found or inaccessible'
}), 500
# Perform cleanup
# Perform cleanup using logger handler
deleted_count = logger_handler.cleanup_old_logs(days_to_keep=days_to_keep)
# Enhanced logging for audit trail
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(
event_type="admin_log_cleanup",
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={
'admin_user': admin_username,
'days_to_keep': days_to_keep,
@@ -2161,7 +2152,7 @@ def api_cleanup_logs():
except Exception as 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({
'success': False,
'error': f'Failed to cleanup old logs: {str(e)}'
+80 -51
View File
@@ -23,7 +23,7 @@ import logging.handlers
import json
import os
import traceback
from datetime import datetime, date
from datetime import datetime, date, timedelta
from functools import wraps
from flask import request, session, g
from sqlalchemy import text
@@ -594,24 +594,48 @@ class AppLogger:
)
# UTILITY METHODS
def get_log_statistics(self, days=7):
"""Get logging statistics for the specified number of days"""
try:
from datetime import datetime, timedelta # Import here as backup
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 = """
SELECT COUNT(*) as total_events
FROM log_events
WHERE created_timestamp >= :cutoff_date
"""
total_result = self.db.session.execute(text(total_sql), {
'cutoff_date': cutoff_date
}).fetchone()
total_result = self.db.session.execute(text(total_sql), {'cutoff_date': cutoff_date}).fetchone()
if total_result:
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
try:
category_sql = """
SELECT
event_category,
@@ -621,36 +645,31 @@ class AppLogger:
GROUP BY event_category
"""
category_result = self.db.session.execute(text(category_sql), {
'cutoff_date': cutoff_date
}).fetchall()
category_result = self.db.session.execute(text(category_sql), {'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:
if row.event_category == 'security':
stats['security_events'] = row.event_count
elif row.event_category == 'database':
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
category = row.event_category
count = row.event_count
print(f"✅ Found {count} events in category: {category}")
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
except Exception as e:
print(f"❌ Error in 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 {
'total_events': 0,
'security_events': 0,
@@ -662,9 +681,22 @@ class AppLogger:
def cleanup_old_logs(self, days_to_keep=90):
"""Clean up old log entries from database"""
try:
from datetime import datetime, timedelta # Import here as backup
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
try:
count_sql = """
SELECT COUNT(*) as count_to_delete
FROM log_events
@@ -672,50 +704,47 @@ class AppLogger:
AND severity_level NOT IN ('ERROR', 'CRITICAL', 'HIGH')
"""
count_result = self.db.session.execute(text(count_sql), {
'cutoff_date': cutoff_date
}).fetchone()
count_result = self.db.session.execute(text(count_sql), {'cutoff_date': cutoff_date}).fetchone()
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:
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
# Perform the cleanup - exclude critical logs
try:
cleanup_sql = """
DELETE FROM log_events
WHERE created_timestamp < :cutoff_date
AND severity_level NOT IN ('ERROR', 'CRITICAL', 'HIGH')
"""
result = self.db.session.execute(text(cleanup_sql), {
'cutoff_date': cutoff_date
})
result = self.db.session.execute(text(cleanup_sql), {'cutoff_date': cutoff_date})
deleted_count = result.rowcount
self.db.session.commit()
# Log the cleanup operation
self.logger.info(f"Log cleanup completed: {deleted_count} entries removed (keeping entries older than {days_to_keep} days)")
print(f"🗑️ Successfully deleted {deleted_count} old log entries")
# Also log to security log for audit
self.log_security_event(
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()
}
)
# Log the cleanup operation
self.logger.info(f"Log cleanup completed: {deleted_count} entries removed (keeping entries newer than {days_to_keep} days)")
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:
print(f"❌ Error in cleanup_old_logs: {e}")
self.db.session.rollback()
self.log_database_error('cleanup_old_logs', e)
print(f"Error in cleanup_old_logs: {e}")
return 0
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 -->
{% extends "base_authenticated.html" %}
{% block title %}System Logs - QR Code Management{% endblock %}
{% block extra_head %}
{% extends "base_authenticated.html" %} {% block title %}System Logs - QR Code
Management{% endblock %} {% block extra_head %}
<!-- Admin Logs CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/admin-logs.css') }}">
{% endblock %}
{% block content %}
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/admin_logs.css') }}"
/>
{% endblock %} {% block content %}
<div class="logs-page">
<!-- Page Header -->
<div class="logs-header">
@@ -22,6 +21,10 @@
<p>Monitor system activities and security events</p>
</div>
<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()">
<i class="fas fa-sync-alt"></i>
Refresh
@@ -34,14 +37,15 @@
</div>
<!-- Log Statistics -->
{% if log_stats %}
<div class="log-stats">
<div class="stat-card total">
<div class="stat-icon">
<i class="fas fa-list"></i>
</div>
<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>
</div>
</div>
@@ -51,7 +55,9 @@
<i class="fas fa-shield-alt"></i>
</div>
<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>
</div>
</div>
@@ -61,7 +67,9 @@
<i class="fas fa-exclamation-triangle"></i>
</div>
<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>
</div>
</div>
@@ -71,11 +79,36 @@
<i class="fas fa-users"></i>
</div>
<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>
</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 %}
<!-- Log Controls -->
@@ -132,13 +165,13 @@
<p>Loading log entries...</p>
</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>
<h3>No Log Entries Found</h3>
<p>No log entries match your current filter criteria.</p>
</div>
<table class="logs-table" id="logsTable" style="display: none;">
<table class="logs-table" id="logsTable" style="display: none">
<thead>
<tr>
<th>Timestamp</th>
@@ -148,6 +181,7 @@
<th>Severity</th>
<th>User</th>
<th>IP Address</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="logsTableBody">
@@ -158,7 +192,7 @@
</div>
<!-- Pagination -->
<div class="pagination-wrapper" id="paginationWrapper" style="display: none;">
<div class="pagination-wrapper" id="paginationWrapper" style="display: none">
<div class="pagination-info">
<span id="paginationInfo">Showing 0 of 0 entries</span>
</div>
@@ -178,13 +212,93 @@
<!-- Cleanup Modal -->
<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-header">
<h3><i class="fas fa-trash-alt"></i> Cleanup Old Logs</h3>
<button class="modal-close" onclick="closeCleanupModal()">&times;</button>
</div>
<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">
<label for="daysToKeep">Keep logs for the last:</label>
<select id="daysToKeep" class="form-control">
@@ -197,11 +311,14 @@
</div>
<div class="warning-note">
<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 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()">
<i class="fas fa-trash-alt"></i>
Delete Old Logs
@@ -216,14 +333,15 @@ let currentPage = 1;
let logsPerPage = 50;
let totalLogs = 0;
let currentFilters = {
search: '',
category: '',
severity: '',
days: 7
search: "",
category: "",
severity: "",
days: 7,
};
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener("DOMContentLoaded", function () {
console.log("Admin logs page loading...");
loadLogs();
setupEventListeners();
});
@@ -231,9 +349,10 @@ document.addEventListener('DOMContentLoaded', function() {
// Setup event listeners
function setupEventListeners() {
// Search input
const searchInput = document.getElementById('searchLogs');
const searchInput = document.getElementById("searchLogs");
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', function() {
searchInput.addEventListener("input", function () {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentFilters.search = this.value;
@@ -241,40 +360,56 @@ function setupEventListeners() {
loadLogs();
}, 500);
});
}
// Filter selects
document.getElementById('categoryFilter').addEventListener('change', function() {
const categoryFilter = document.getElementById("categoryFilter");
if (categoryFilter) {
categoryFilter.addEventListener("change", function () {
currentFilters.category = this.value;
currentPage = 1;
loadLogs();
});
}
document.getElementById('severityFilter').addEventListener('change', function() {
const severityFilter = document.getElementById("severityFilter");
if (severityFilter) {
severityFilter.addEventListener("change", function () {
currentFilters.severity = this.value;
currentPage = 1;
loadLogs();
});
}
document.getElementById('daysFilter').addEventListener('change', function() {
const daysFilter = document.getElementById("daysFilter");
if (daysFilter) {
daysFilter.addEventListener("change", function () {
currentFilters.days = parseInt(this.value);
currentPage = 1;
loadLogs();
loadStats();
});
}
}
// Load logs from API
async function loadLogs() {
console.log("Loading logs...");
showLoading();
try {
const params = new URLSearchParams({
days: currentFilters.days,
limit: logsPerPage,
page: currentPage
page: currentPage,
});
const response = await fetch(`/api/logs/recent?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.success) {
@@ -282,32 +417,36 @@ async function loadLogs() {
totalLogs = data.total || data.logs.length;
updatePagination();
} else {
showError('Failed to load logs: ' + (data.error || 'Unknown error'));
showError("Failed to load logs: " + (data.error || "Unknown error"));
}
} catch (error) {
console.error('Error loading logs:', error);
showError('Failed to load logs. Please try again.');
console.error("Error loading logs:", error);
showError("Failed to load logs: " + error.message);
}
}
// Load log statistics
async function loadStats() {
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();
if (data.success && data.stats) {
updateStatsDisplay(data.stats);
} else {
console.error("Failed to load stats:", data.error);
}
} catch (error) {
console.error('Error loading stats:', error);
console.error("Error loading stats:", error);
}
}
// Display logs in table
function displayLogs(logs) {
const tbody = document.getElementById('logsTableBody');
tbody.innerHTML = '';
const tbody = document.getElementById("logsTableBody");
tbody.innerHTML = "";
if (logs.length === 0) {
showEmpty();
@@ -315,17 +454,22 @@ function displayLogs(logs) {
}
// Filter logs based on current filters
let filteredLogs = logs.filter(log => {
let filteredLogs = logs.filter((log) => {
if (currentFilters.search) {
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.username && log.username.toLowerCase().includes(searchLower))) {
!(log.username && log.username.toLowerCase().includes(searchLower))
) {
return false;
}
}
if (currentFilters.category && log.event_category !== currentFilters.category) {
if (
currentFilters.category &&
log.event_category !== currentFilters.category
) {
return false;
}
@@ -336,88 +480,244 @@ function displayLogs(logs) {
return true;
});
filteredLogs.forEach(log => {
const row = document.createElement('tr');
filteredLogs.forEach((log, index) => {
const row = document.createElement("tr");
row.className = getSeverityClass(log.severity);
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 = `
<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">
<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 class="description">${escapeHtml(log.description)}</td>
<td class="description" title="${escapeHtml(
log.description || ""
)}">${escapeHtml(shortDescription)}</td>
<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 class="username">${log.username ? escapeHtml(log.username) : '<em>System</em>'}</td>
<td class="ip-address">${log.ip_address || '-'}</td>
`;
tbody.appendChild(row);
});
// Store filtered logs globally for details modal
window.currentLogs = filteredLogs;
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
function getSeverityClass(severity) {
switch (severity) {
case 'HIGH': return 'severity-high';
case 'MEDIUM': return 'severity-medium';
case 'LOW': return 'severity-low';
default: return 'severity-info';
switch ((severity || "info").toLowerCase()) {
case "high":
return "severity-high";
case "medium":
return "severity-medium";
case "low":
return "severity-low";
default:
return "severity-info";
}
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
if (!text) return "";
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function showLoading() {
document.getElementById('loadingState').style.display = 'block';
document.getElementById('emptyState').style.display = 'none';
document.getElementById('logsTable').style.display = 'none';
document.getElementById('paginationWrapper').style.display = 'none';
document.getElementById("loadingState").style.display = "block";
document.getElementById("emptyState").style.display = "none";
document.getElementById("logsTable").style.display = "none";
document.getElementById("paginationWrapper").style.display = "none";
}
function showEmpty() {
document.getElementById('loadingState').style.display = 'none';
document.getElementById('emptyState').style.display = 'block';
document.getElementById('logsTable').style.display = 'none';
document.getElementById('paginationWrapper').style.display = 'none';
document.getElementById("loadingState").style.display = "none";
document.getElementById("emptyState").style.display = "block";
document.getElementById("logsTable").style.display = "none";
document.getElementById("paginationWrapper").style.display = "none";
}
function showTable() {
document.getElementById('loadingState').style.display = 'none';
document.getElementById('emptyState').style.display = 'none';
document.getElementById('logsTable').style.display = 'table';
document.getElementById('paginationWrapper').style.display = 'flex';
document.getElementById("loadingState").style.display = "none";
document.getElementById("emptyState").style.display = "none";
document.getElementById("logsTable").style.display = "table";
document.getElementById("paginationWrapper").style.display = "flex";
}
function showError(message) {
// Simple error display - you could enhance this with a proper toast/notification system
alert(message);
showMessage("❌ " + message, "error");
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
function updatePagination() {
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 end = Math.min(currentPage * logsPerPage, totalLogs);
info.textContent = `Showing ${start}-${end} of ${totalLogs} entries`;
}
document.getElementById('prevPage').disabled = currentPage <= 1;
document.getElementById('nextPage').disabled = currentPage >= totalPages;
const prevBtn = document.getElementById("prevPage");
const nextBtn = document.getElementById("nextPage");
if (prevBtn) prevBtn.disabled = currentPage <= 1;
if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
}
// Pagination controls
@@ -439,64 +739,92 @@ function refreshLogs() {
loadStats();
}
// Export logs
function exportLogs() {
const params = new URLSearchParams({
days: currentFilters.days,
format: 'csv'
});
// Test logging functionality
async function testLogging() {
try {
showMessage("Testing logging system...", "info");
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() {
document.getElementById('cleanupModal').style.display = 'flex';
document.getElementById("cleanupModal").style.display = "flex";
}
function closeCleanupModal() {
document.getElementById('cleanupModal').style.display = 'none';
document.getElementById("cleanupModal").style.display = "none";
}
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 {
const response = await fetch('/api/logs/cleanup', {
method: 'POST',
showMessage("Cleaning up old logs...", "info");
const response = await fetch("/api/logs/cleanup", {
method: "POST",
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();
if (data.success) {
alert(`Successfully cleaned up ${data.deleted_count} old log entries.`);
showSuccess(
`Successfully cleaned up ${data.deleted_count} old log entries`
);
closeCleanupModal();
setTimeout(() => {
loadLogs();
loadStats();
}, 1000);
} else {
alert('Error cleaning up logs: ' + (data.error || 'Unknown error'));
showError("Error cleaning up logs: " + (data.error || "Unknown error"));
}
} catch (error) {
console.error('Error cleaning up logs:', error);
alert('Failed to cleanup logs. Please try again.');
console.error("Error cleaning up logs:", error);
showError("Failed to cleanup logs. Please try again.");
}
}
function updateStatsDisplay(stats) {
// Update stat cards if they exist
const totalElement = document.querySelector('.stat-card.total h3');
const securityElement = document.querySelector('.stat-card.security h3');
const errorsElement = document.querySelector('.stat-card.errors h3');
const usersElement = document.querySelector('.stat-card.users h3');
const totalElement = document.getElementById("totalEventsCount");
const securityElement = document.getElementById("securityEventsCount");
const errorsElement = document.getElementById("databaseErrorsCount");
const usersElement = document.getElementById("userActivitiesCount");
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 (usersElement) usersElement.textContent = stats.user_activities || 0;
console.log("Stats updated:", stats);
}
</script>
@@ -504,7 +832,7 @@ function updateStatsDisplay(stats) {
/* Admin Logs Styles */
.logs-page {
padding: var(--spacing-6);
max-width: 1400px;
max-width: 1600px;
margin: 0 auto;
}
@@ -585,7 +913,11 @@ function updateStatsDisplay(stats) {
justify-content: center;
font-size: var(--font-size-xl);
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 {
@@ -639,7 +971,8 @@ function updateStatsDisplay(stats) {
.search-box input {
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-radius: var(--radius);
font-size: var(--font-size-sm);
+2 -4
View File
@@ -24,7 +24,7 @@
<link
rel="icon"
type="image/x-icon"
href="{{ url_for('static', filename='favicon.ico') }}"
href="{{ url_for('static', filename='images/favicon.ico') }}"
/>
</head>
<body class="login-layout">
@@ -52,9 +52,7 @@
{% endif %} {% endwith %}
<!-- Page Content -->
<div class="container">
{% block content %}{% endblock %}
</div>
<div class="container">{% block content %}{% endblock %}</div>
</main>
<!-- Footer -->