From 7114ed24c9053254a38b492d7f93e933f1633944 Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Sat, 16 Aug 2025 15:07:55 -0400 Subject: [PATCH] Update logs page: pagination --- app.py | 72 +- static/css/admin_logs.css | 77 +- templates/admin_logs.html | 2408 ++++++++++++++++++++----------------- 3 files changed, 1412 insertions(+), 1145 deletions(-) diff --git a/app.py b/app.py index 80eac14..ca16849 100644 --- a/app.py +++ b/app.py @@ -5,8 +5,7 @@ from functools import wraps from datetime import datetime, date, time, timedelta from sqlalchemy import text from user_agents import parse -from math import radians, cos, sin, asin, sqrt -import io, os, base64, re, uuid, requests, json, qrcode +import io, os, base64, re, uuid, requests, json, qrcode, math from dotenv import load_dotenv # Import the logging handler from logger_handler import AppLogger, log_user_activity, log_database_operations @@ -1988,15 +1987,25 @@ def admin_logs(): @app.route('/api/logs/recent') @admin_required def api_recent_logs(): - """API endpoint to get recent log entries with full details""" + """API endpoint to get recent log entries with full details and pagination support""" try: days = request.args.get('days', 1, type=int) limit = request.args.get('limit', 50, type=int) + page = request.args.get('page', 1, type=int) + category = request.args.get('category', '') + severity = request.args.get('severity', '') + search = request.args.get('search', '') + + print(f"📊 API request - Days: {days}, Limit: {limit}, Page: {page}") + print(f"📊 Filters - Category: {category}, Severity: {severity}, Search: {search}") cutoff_date = datetime.now() - timedelta(days=days) - # Enhanced SQL to get all available fields - logs_sql = """ + # Calculate offset for pagination + offset = (page - 1) * limit + + # Build the base SQL query with filters + base_sql = """ SELECT event_id, event_type, @@ -2010,14 +2019,46 @@ def api_recent_logs(): ip_address FROM log_events WHERE created_timestamp >= :cutoff_date - ORDER BY created_timestamp DESC - LIMIT :limit """ - result = db.session.execute(text(logs_sql), { - 'cutoff_date': cutoff_date, - 'limit': limit - }).fetchall() + count_sql = """ + SELECT COUNT(*) as total_count + FROM log_events + WHERE created_timestamp >= :cutoff_date + """ + + params = {'cutoff_date': cutoff_date} + + # Add category filter + if category: + base_sql += " AND event_category = :category" + count_sql += " AND event_category = :category" + params['category'] = category + + # Add severity filter + if severity: + base_sql += " AND severity_level = :severity" + count_sql += " AND severity_level = :severity" + params['severity'] = severity + + # Add search filter + if search: + search_condition = " AND (event_type LIKE :search OR event_description LIKE :search OR username LIKE :search)" + base_sql += search_condition + count_sql += search_condition + params['search'] = f'%{search}%' + + # Get total count first + count_result = db.session.execute(text(count_sql), params).fetchone() + total_count = count_result.total_count if count_result else 0 + + # Add ordering, limit and offset to main query + base_sql += " ORDER BY created_timestamp DESC LIMIT :limit OFFSET :offset" + params['limit'] = limit + params['offset'] = offset + + # Execute main query + result = db.session.execute(text(base_sql), params).fetchall() logs = [] for row in result: @@ -2042,10 +2083,17 @@ def api_recent_logs(): 'ip_address': row.ip_address or '-' }) + print(f"📊 Returning {len(logs)} logs out of {total_count} total") + return jsonify({ 'success': True, 'logs': logs, - 'total': len(logs) + 'total': total_count, + 'page': page, + 'limit': limit, + 'total_pages': math.ceil(total_count / limit) if total_count > 0 else 0, + 'has_next': offset + limit < total_count, + 'has_prev': page > 1 }) except Exception as e: diff --git a/static/css/admin_logs.css b/static/css/admin_logs.css index 6137492..e613e46 100644 --- a/static/css/admin_logs.css +++ b/static/css/admin_logs.css @@ -82,7 +82,11 @@ 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 { @@ -229,7 +233,7 @@ } .timestamp { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace; font-size: var(--font-size-xs); color: var(--gray-600); white-space: nowrap; @@ -314,7 +318,7 @@ } .ip-address { - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace; font-size: var(--font-size-xs); color: var(--gray-500); min-width: 120px; @@ -336,8 +340,12 @@ } @keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } .empty-state i { @@ -397,6 +405,39 @@ background: var(--gray-100); } +/* Page number buttons */ +.page-number { + min-width: 35px !important; + margin: 0 2px; + padding: var(--spacing-1) var(--spacing-2) !important; + border: 1px solid var(--gray-300); + background: var(--white); + color: var(--gray-700); +} + +.page-number:hover:not(.active) { + background: var(--gray-50); + border-color: var(--primary-color); +} + +.page-number.active { + background: var(--primary-color) !important; + color: var(--white) !important; + border-color: var(--primary-color) !important; +} + +.pagination-ellipsis { + padding: var(--spacing-1) var(--spacing-2); + color: var(--gray-500); + font-size: var(--font-size-sm); +} + +.pagination-numbers { + display: flex; + align-items: center; + gap: var(--spacing-1); +} + /* Modal Styles */ .modal { display: none; @@ -693,10 +734,18 @@ animation: fadeInUp 0.6s ease-out; } -.stat-card:nth-child(1) { animation-delay: 0.1s; } -.stat-card:nth-child(2) { animation-delay: 0.2s; } -.stat-card:nth-child(3) { animation-delay: 0.3s; } -.stat-card:nth-child(4) { animation-delay: 0.4s; } +.stat-card:nth-child(1) { + animation-delay: 0.1s; +} +.stat-card:nth-child(2) { + animation-delay: 0.2s; +} +.stat-card:nth-child(3) { + animation-delay: 0.3s; +} +.stat-card:nth-child(4) { + animation-delay: 0.4s; +} @keyframes fadeInUp { from { @@ -714,8 +763,12 @@ } @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + to { + opacity: 1; + } } /* Focus and accessibility improvements */ @@ -748,4 +801,4 @@ .logs-table td { padding: 4px; } -} \ No newline at end of file +} diff --git a/templates/admin_logs.html b/templates/admin_logs.html index aa90b61..e22b459 100644 --- a/templates/admin_logs.html +++ b/templates/admin_logs.html @@ -184,386 +184,476 @@ Management{% endblock %} {% block extra_head %} Previous - + + + - - -