Update logs page: pagination
This commit is contained in:
@@ -5,8 +5,7 @@ from functools import wraps
|
|||||||
from datetime import datetime, date, time, timedelta
|
from datetime import datetime, date, time, timedelta
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from user_agents import parse
|
from user_agents import parse
|
||||||
from math import radians, cos, sin, asin, sqrt
|
import io, os, base64, re, uuid, requests, json, qrcode, math
|
||||||
import io, os, base64, re, uuid, requests, json, qrcode
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
# Import the logging handler
|
# Import the logging handler
|
||||||
from logger_handler import AppLogger, log_user_activity, log_database_operations
|
from logger_handler import AppLogger, log_user_activity, log_database_operations
|
||||||
@@ -1988,15 +1987,25 @@ 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():
|
||||||
"""API endpoint to get recent log entries with full details"""
|
"""API endpoint to get recent log entries with full details and pagination support"""
|
||||||
try:
|
try:
|
||||||
days = request.args.get('days', 1, type=int)
|
days = request.args.get('days', 1, type=int)
|
||||||
limit = request.args.get('limit', 50, 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)
|
cutoff_date = datetime.now() - timedelta(days=days)
|
||||||
|
|
||||||
# Enhanced SQL to get all available fields
|
# Calculate offset for pagination
|
||||||
logs_sql = """
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
|
# Build the base SQL query with filters
|
||||||
|
base_sql = """
|
||||||
SELECT
|
SELECT
|
||||||
event_id,
|
event_id,
|
||||||
event_type,
|
event_type,
|
||||||
@@ -2010,14 +2019,46 @@ def api_recent_logs():
|
|||||||
ip_address
|
ip_address
|
||||||
FROM log_events
|
FROM log_events
|
||||||
WHERE created_timestamp >= :cutoff_date
|
WHERE created_timestamp >= :cutoff_date
|
||||||
ORDER BY created_timestamp DESC
|
|
||||||
LIMIT :limit
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
result = db.session.execute(text(logs_sql), {
|
count_sql = """
|
||||||
'cutoff_date': cutoff_date,
|
SELECT COUNT(*) as total_count
|
||||||
'limit': limit
|
FROM log_events
|
||||||
}).fetchall()
|
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 = []
|
logs = []
|
||||||
for row in result:
|
for row in result:
|
||||||
@@ -2042,10 +2083,17 @@ def api_recent_logs():
|
|||||||
'ip_address': row.ip_address or '-'
|
'ip_address': row.ip_address or '-'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
print(f"📊 Returning {len(logs)} logs out of {total_count} total")
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'logs': logs,
|
'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:
|
except Exception as e:
|
||||||
|
|||||||
+64
-11
@@ -82,7 +82,11 @@
|
|||||||
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 {
|
||||||
@@ -229,7 +233,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timestamp {
|
.timestamp {
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
color: var(--gray-600);
|
color: var(--gray-600);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -314,7 +318,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ip-address {
|
.ip-address {
|
||||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
color: var(--gray-500);
|
color: var(--gray-500);
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
@@ -336,8 +340,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
from { transform: rotate(0deg); }
|
from {
|
||||||
to { transform: rotate(360deg); }
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state i {
|
.empty-state i {
|
||||||
@@ -397,6 +405,39 @@
|
|||||||
background: var(--gray-100);
|
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 Styles */
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -693,10 +734,18 @@
|
|||||||
animation: fadeInUp 0.6s ease-out;
|
animation: fadeInUp 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card:nth-child(1) { animation-delay: 0.1s; }
|
.stat-card:nth-child(1) {
|
||||||
.stat-card:nth-child(2) { animation-delay: 0.2s; }
|
animation-delay: 0.1s;
|
||||||
.stat-card:nth-child(3) { animation-delay: 0.3s; }
|
}
|
||||||
.stat-card:nth-child(4) { animation-delay: 0.4s; }
|
.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 {
|
@keyframes fadeInUp {
|
||||||
from {
|
from {
|
||||||
@@ -714,8 +763,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from {
|
||||||
to { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus and accessibility improvements */
|
/* Focus and accessibility improvements */
|
||||||
|
|||||||
+186
-20
@@ -184,17 +184,18 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
<i class="fas fa-chevron-left"></i>
|
<i class="fas fa-chevron-left"></i>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</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()">
|
<button class="btn btn-sm" id="nextPage" onclick="nextPage()">
|
||||||
Next
|
Next
|
||||||
<i class="fas fa-chevron-right"></i>
|
<i class="fas fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Cleanup Modal -->
|
<!-- Cleanup Modal -->
|
||||||
<div class="modal" id="cleanupModal">
|
<div class="modal" id="cleanupModal">
|
||||||
<div class="modal" id="logDetailsModal">
|
<div class="modal" id="logDetailsModal">
|
||||||
<div class="modal-content log-details-modal">
|
<div class="modal-content log-details-modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
@@ -275,12 +276,14 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
<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()">×</button>
|
<button class="modal-close" onclick="closeCleanupModal()">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>
|
<p>
|
||||||
This action will permanently delete log entries older than the specified
|
This action will permanently delete log entries older than the
|
||||||
number of days.
|
specified number of days.
|
||||||
</p>
|
</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>
|
||||||
@@ -308,9 +311,9 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Global variables
|
// Global variables
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let logsPerPage = 50;
|
let logsPerPage = 50;
|
||||||
@@ -326,6 +329,7 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
console.log("Admin logs page loading...");
|
console.log("Admin logs page loading...");
|
||||||
loadLogs();
|
loadLogs();
|
||||||
|
loadStats();
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -377,7 +381,10 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
|
|
||||||
// Load logs from API
|
// Load logs from API
|
||||||
async function loadLogs() {
|
async function loadLogs() {
|
||||||
console.log("Loading logs...");
|
console.log(
|
||||||
|
`Loading logs - Page: ${currentPage}, Filters:`,
|
||||||
|
currentFilters
|
||||||
|
);
|
||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -387,6 +394,17 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
page: currentPage,
|
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}`);
|
const response = await fetch(`/api/logs/recent?${params}`);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -397,8 +415,19 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
displayLogs(data.logs);
|
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();
|
updatePagination();
|
||||||
|
|
||||||
|
if (data.logs.length > 0) {
|
||||||
|
showTable();
|
||||||
|
} else {
|
||||||
|
showEmpty();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
showError("Failed to load logs: " + (data.error || "Unknown error"));
|
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, """)})">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`📊 Displayed ${logs.length} log entries`);
|
||||||
|
}
|
||||||
|
|
||||||
// Load log statistics
|
// Load log statistics
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
@@ -538,7 +625,10 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentFilters.severity && log.severity !== currentFilters.severity) {
|
if (
|
||||||
|
currentFilters.severity &&
|
||||||
|
log.severity !== currentFilters.severity
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,7 +688,8 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
if (!log) return;
|
if (!log) return;
|
||||||
|
|
||||||
// Populate modal with log details
|
// Populate modal with log details
|
||||||
document.getElementById("detailEventId").textContent = log.event_id || "-";
|
document.getElementById("detailEventId").textContent =
|
||||||
|
log.event_id || "-";
|
||||||
document.getElementById("detailEventType").textContent =
|
document.getElementById("detailEventType").textContent =
|
||||||
log.event_type || "-";
|
log.event_type || "-";
|
||||||
document.getElementById("detailCategory").textContent =
|
document.getElementById("detailCategory").textContent =
|
||||||
@@ -773,16 +864,62 @@ ${
|
|||||||
const info = document.getElementById("paginationInfo");
|
const info = document.getElementById("paginationInfo");
|
||||||
|
|
||||||
if (info) {
|
if (info) {
|
||||||
const start = (currentPage - 1) * logsPerPage + 1;
|
const start = totalLogs > 0 ? (currentPage - 1) * logsPerPage + 1 : 0;
|
||||||
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`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevBtn = document.getElementById("prevPage");
|
const prevBtn = document.getElementById("prevPage");
|
||||||
const nextBtn = document.getElementById("nextPage");
|
const nextBtn = document.getElementById("nextPage");
|
||||||
|
const pageNumbers = document.getElementById("pageNumbers");
|
||||||
|
|
||||||
if (prevBtn) prevBtn.disabled = currentPage <= 1;
|
if (prevBtn) prevBtn.disabled = currentPage <= 1;
|
||||||
if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
|
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
|
// Pagination controls
|
||||||
@@ -794,8 +931,32 @@ ${
|
|||||||
}
|
}
|
||||||
|
|
||||||
function nextPage() {
|
function nextPage() {
|
||||||
|
const totalPages = Math.ceil(totalLogs / logsPerPage);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Next page requested. Current: ${currentPage}, Total Pages: ${totalPages}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentPage < totalPages) {
|
||||||
currentPage++;
|
currentPage++;
|
||||||
loadLogs();
|
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
|
// Refresh logs
|
||||||
@@ -844,7 +1005,9 @@ ${
|
|||||||
loadStats();
|
loadStats();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
showError("❌ Error clearing logs: " + (data.error || "Unknown error"));
|
showError(
|
||||||
|
"❌ Error clearing logs: " + (data.error || "Unknown error")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error clearing logs:", error);
|
console.error("Error clearing logs:", error);
|
||||||
@@ -892,7 +1055,9 @@ ${
|
|||||||
loadStats();
|
loadStats();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
showError("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);
|
||||||
@@ -914,9 +1079,9 @@ ${
|
|||||||
|
|
||||||
console.log("Stats updated:", stats);
|
console.log("Stats updated:", stats);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Admin Logs Styles */
|
/* Admin Logs Styles */
|
||||||
.logs-page {
|
.logs-page {
|
||||||
padding: var(--spacing-6);
|
padding: var(--spacing-6);
|
||||||
@@ -1437,5 +1602,6 @@ ${
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user