Update logs page: pagination

This commit is contained in:
2025-08-16 15:07:55 -04:00
parent 4b5d21bc73
commit 7114ed24c9
3 changed files with 1412 additions and 1145 deletions
+60 -12
View File
@@ -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:
+64 -11
View File
@@ -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 */
+178 -12
View File
@@ -184,14 +184,15 @@ Management{% endblock %} {% block extra_head %}
<i class="fas fa-chevron-left"></i>
Previous
</button>
<span class="pagination-numbers" id="pageNumbers"></span>
<span class="pagination-numbers" id="pageNumbers">
<!-- Page numbers will be inserted here -->
</span>
<button class="btn btn-sm" id="nextPage" onclick="nextPage()">
Next
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
</div>
<!-- Cleanup Modal -->
<div class="modal" id="cleanupModal">
@@ -275,12 +276,14 @@ Management{% endblock %} {% block extra_head %}
<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>
<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.
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>
@@ -326,6 +329,7 @@ Management{% endblock %} {% block extra_head %}
document.addEventListener("DOMContentLoaded", function () {
console.log("Admin logs page loading...");
loadLogs();
loadStats();
setupEventListeners();
});
@@ -377,7 +381,10 @@ Management{% endblock %} {% block extra_head %}
// Load logs from API
async function loadLogs() {
console.log("Loading logs...");
console.log(
`Loading logs - Page: ${currentPage}, Filters:`,
currentFilters
);
showLoading();
try {
@@ -387,6 +394,17 @@ Management{% endblock %} {% block extra_head %}
page: currentPage,
});
// Add filters to API request
if (currentFilters.category) {
params.append("category", currentFilters.category);
}
if (currentFilters.severity) {
params.append("severity", currentFilters.severity);
}
if (currentFilters.search) {
params.append("search", currentFilters.search);
}
const response = await fetch(`/api/logs/recent?${params}`);
if (!response.ok) {
@@ -397,8 +415,19 @@ Management{% endblock %} {% block extra_head %}
if (data.success) {
displayLogs(data.logs);
totalLogs = data.total || data.logs.length;
totalLogs = data.total; // Use actual total count from backend
console.log(
`📊 Loaded ${data.logs.length} logs, Total: ${data.total}, Page: ${data.page}`
);
updatePagination();
if (data.logs.length > 0) {
showTable();
} else {
showEmpty();
}
} else {
showError("Failed to load logs: " + (data.error || "Unknown error"));
}
@@ -408,6 +437,64 @@ Management{% endblock %} {% block extra_head %}
}
}
// Display logs in table
function displayLogs(logs) {
const tbody = document.getElementById("logsTableBody");
tbody.innerHTML = "";
if (logs.length === 0) {
showEmpty();
return;
}
// No need to filter here - filtering is now done on backend
logs.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 || "Unknown")}</td>
<td class="category">
<span class="category-badge ${log.event_category || "system"}">${
log.event_category || "system"
}</span>
</td>
<td class="description" title="${escapeHtml(
log.description || ""
)}">${escapeHtml(shortDescription)}</td>
<td class="severity">
<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">${escapeHtml(log.ip_address || "-")}</td>
<td class="actions">
<button class="btn btn-sm btn-secondary" onclick="viewLogDetails(${JSON.stringify(
log
).replace(/"/g, "&quot;")})">
<i class="fas fa-eye"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
console.log(`📊 Displayed ${logs.length} log entries`);
}
// Load log statistics
async function loadStats() {
try {
@@ -538,7 +625,10 @@ Management{% endblock %} {% block extra_head %}
return false;
}
if (currentFilters.severity && log.severity !== currentFilters.severity) {
if (
currentFilters.severity &&
log.severity !== currentFilters.severity
) {
return false;
}
@@ -598,7 +688,8 @@ Management{% endblock %} {% block extra_head %}
if (!log) return;
// Populate modal with log details
document.getElementById("detailEventId").textContent = log.event_id || "-";
document.getElementById("detailEventId").textContent =
log.event_id || "-";
document.getElementById("detailEventType").textContent =
log.event_type || "-";
document.getElementById("detailCategory").textContent =
@@ -773,16 +864,62 @@ ${
const info = document.getElementById("paginationInfo");
if (info) {
const start = (currentPage - 1) * logsPerPage + 1;
const start = totalLogs > 0 ? (currentPage - 1) * logsPerPage + 1 : 0;
const end = Math.min(currentPage * logsPerPage, totalLogs);
info.textContent = `Showing ${start}-${end} of ${totalLogs} entries`;
}
const prevBtn = document.getElementById("prevPage");
const nextBtn = document.getElementById("nextPage");
const pageNumbers = document.getElementById("pageNumbers");
if (prevBtn) prevBtn.disabled = currentPage <= 1;
if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
// Generate page numbers
if (pageNumbers && totalPages > 1) {
let pageNumbersHTML = "";
// Show page numbers (max 5 visible)
const maxVisible = 5;
let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2));
let endPage = Math.min(totalPages, startPage + maxVisible - 1);
// Adjust start if we're near the end
if (endPage - startPage + 1 < maxVisible) {
startPage = Math.max(1, endPage - maxVisible + 1);
}
// First page + ellipsis
if (startPage > 1) {
pageNumbersHTML += `<button class="btn btn-sm page-number" onclick="goToPage(1)">1</button>`;
if (startPage > 2) {
pageNumbersHTML += `<span class="pagination-ellipsis">...</span>`;
}
}
// Page numbers
for (let i = startPage; i <= endPage; i++) {
const isActive = i === currentPage ? " active" : "";
pageNumbersHTML += `<button class="btn btn-sm page-number${isActive}" onclick="goToPage(${i})">${i}</button>`;
}
// Last page + ellipsis
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
pageNumbersHTML += `<span class="pagination-ellipsis">...</span>`;
}
pageNumbersHTML += `<button class="btn btn-sm page-number" onclick="goToPage(${totalPages})">${totalPages}</button>`;
}
pageNumbers.innerHTML = pageNumbersHTML;
} else if (pageNumbers) {
pageNumbers.innerHTML = "";
}
console.log(
`📊 Pagination updated - Page ${currentPage}/${totalPages}, Total logs: ${totalLogs}`
);
}
// Pagination controls
@@ -794,8 +931,32 @@ ${
}
function nextPage() {
const totalPages = Math.ceil(totalLogs / logsPerPage);
console.log(
`Next page requested. Current: ${currentPage}, Total Pages: ${totalPages}`
);
if (currentPage < totalPages) {
currentPage++;
loadLogs();
} else {
console.log("Already on last page");
}
}
// Go to specific page
function goToPage(page) {
const totalPages = Math.ceil(totalLogs / logsPerPage);
if (page < 1 || page > totalPages) {
console.log(`Invalid page: ${page}. Valid range: 1-${totalPages}`);
return;
}
console.log(`Going to page ${page}`);
currentPage = page;
loadLogs();
}
// Refresh logs
@@ -844,7 +1005,9 @@ ${
loadStats();
}, 1000);
} else {
showError("❌ Error clearing logs: " + (data.error || "Unknown error"));
showError(
"❌ Error clearing logs: " + (data.error || "Unknown error")
);
}
} catch (error) {
console.error("Error clearing logs:", error);
@@ -892,7 +1055,9 @@ ${
loadStats();
}, 1000);
} else {
showError("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);
@@ -1439,3 +1604,4 @@ ${
}
</style>
{% endblock %}
</div>