Files
GOV_QR_Codes_Management/templates/admin_logs.html
T

1020 lines
23 KiB
HTML

<!-- templates/admin/logs.html -->
{% 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 %}
<div class="logs-page">
<!-- Page Header -->
<div class="logs-header">
<div class="header-content">
<div class="header-navigation">
<a href="{{ url_for('dashboard') }}" class="back-button">
<i class="fas fa-arrow-left"></i>
<span>Back to Dashboard</span>
</a>
</div>
<h1><i class="fas fa-clipboard-list"></i> System Logs</h1>
<p>Monitor system activities and security events</p>
</div>
<div class="header-actions">
<button class="btn btn-secondary" onclick="refreshLogs()">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
<button class="btn btn-warning" onclick="cleanupLogs()">
<i class="fas fa-trash-alt"></i>
Cleanup Old Logs
</button>
</div>
</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>
<p>Total Events (7 days)</p>
</div>
</div>
<div class="stat-card security">
<div class="stat-icon security">
<i class="fas fa-shield-alt"></i>
</div>
<div class="stat-info">
<h3>{{ log_stats.security_events or 0 }}</h3>
<p>Security Events</p>
</div>
</div>
<div class="stat-card errors">
<div class="stat-icon errors">
<i class="fas fa-exclamation-triangle"></i>
</div>
<div class="stat-info">
<h3>{{ log_stats.database_errors or 0 }}</h3>
<p>Database Errors</p>
</div>
</div>
<div class="stat-card users">
<div class="stat-icon users">
<i class="fas fa-users"></i>
</div>
<div class="stat-info">
<h3>{{ log_stats.user_activities or 0 }}</h3>
<p>User Activities</p>
</div>
</div>
</div>
{% endif %}
<!-- Log Controls -->
<div class="log-controls">
<div class="search-filters">
<div class="search-box">
<i class="fas fa-search"></i>
<input
type="text"
id="searchLogs"
placeholder="Search logs by event type, description, or username..."
/>
</div>
<div class="filter-group">
<select id="categoryFilter" class="filter-select">
<option value="">All Categories</option>
<option value="security">Security Events</option>
<option value="database">Database Events</option>
<option value="user_activity">User Activities</option>
<option value="system">System Events</option>
</select>
<select id="severityFilter" class="filter-select">
<option value="">All Severity</option>
<option value="HIGH">High</option>
<option value="MEDIUM">Medium</option>
<option value="LOW">Low</option>
<option value="INFO">Info</option>
</select>
<select id="daysFilter" class="filter-select">
<option value="1">Last 24 Hours</option>
<option value="7" selected>Last 7 Days</option>
<option value="30">Last 30 Days</option>
<option value="90">Last 90 Days</option>
</select>
</div>
</div>
<div class="view-options">
<button class="btn btn-secondary btn-sm" onclick="exportLogs()">
<i class="fas fa-download"></i>
Export
</button>
</div>
</div>
<!-- Logs Table -->
<div class="logs-table-container">
<div id="logsTableWrapper">
<div class="loading-state" id="loadingState">
<i class="fas fa-spinner fa-spin"></i>
<p>Loading log entries...</p>
</div>
<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;">
<thead>
<tr>
<th>Timestamp</th>
<th>Event Type</th>
<th>Category</th>
<th>Description</th>
<th>Severity</th>
<th>User</th>
<th>IP Address</th>
</tr>
</thead>
<tbody id="logsTableBody">
<!-- Logs will be loaded via JavaScript -->
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
<div class="pagination-wrapper" id="paginationWrapper" style="display: none;">
<div class="pagination-info">
<span id="paginationInfo">Showing 0 of 0 entries</span>
</div>
<div class="pagination-controls">
<button class="btn btn-sm" id="prevPage" onclick="previousPage()">
<i class="fas fa-chevron-left"></i>
Previous
</button>
<span class="pagination-numbers" id="pageNumbers"></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">
<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>
<div class="form-group">
<label for="daysToKeep">Keep logs for the last:</label>
<select id="daysToKeep" class="form-control">
<option value="30">30 days</option>
<option value="60">60 days</option>
<option value="90" selected>90 days</option>
<option value="180">180 days</option>
<option value="365">1 year</option>
</select>
</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.
</div>
</div>
<div class="modal-footer">
<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
</button>
</div>
</div>
</div>
<script>
// Global variables
let currentPage = 1;
let logsPerPage = 50;
let totalLogs = 0;
let currentFilters = {
search: '',
category: '',
severity: '',
days: 7
};
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
loadLogs();
setupEventListeners();
});
// Setup event listeners
function setupEventListeners() {
// Search input
const searchInput = document.getElementById('searchLogs');
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentFilters.search = this.value;
currentPage = 1;
loadLogs();
}, 500);
});
// Filter selects
document.getElementById('categoryFilter').addEventListener('change', function() {
currentFilters.category = this.value;
currentPage = 1;
loadLogs();
});
document.getElementById('severityFilter').addEventListener('change', function() {
currentFilters.severity = this.value;
currentPage = 1;
loadLogs();
});
document.getElementById('daysFilter').addEventListener('change', function() {
currentFilters.days = parseInt(this.value);
currentPage = 1;
loadLogs();
loadStats();
});
}
// Load logs from API
async function loadLogs() {
showLoading();
try {
const params = new URLSearchParams({
days: currentFilters.days,
limit: logsPerPage,
page: currentPage
});
const response = await fetch(`/api/logs/recent?${params}`);
const data = await response.json();
if (data.success) {
displayLogs(data.logs);
totalLogs = data.total || data.logs.length;
updatePagination();
} else {
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.');
}
}
// Load log statistics
async function loadStats() {
try {
const response = await fetch(`/api/logs/stats?days=${currentFilters.days}`);
const data = await response.json();
if (data.success && data.stats) {
updateStatsDisplay(data.stats);
}
} catch (error) {
console.error('Error loading stats:', error);
}
}
// Display logs in table
function displayLogs(logs) {
const tbody = document.getElementById('logsTableBody');
tbody.innerHTML = '';
if (logs.length === 0) {
showEmpty();
return;
}
// Filter logs based on current filters
let filteredLogs = logs.filter(log => {
if (currentFilters.search) {
const searchLower = currentFilters.search.toLowerCase();
if (!log.event_type.toLowerCase().includes(searchLower) &&
!log.description.toLowerCase().includes(searchLower) &&
!(log.username && log.username.toLowerCase().includes(searchLower))) {
return false;
}
}
if (currentFilters.category && log.event_category !== currentFilters.category) {
return false;
}
if (currentFilters.severity && log.severity !== currentFilters.severity) {
return false;
}
return true;
});
filteredLogs.forEach(log => {
const row = document.createElement('tr');
row.className = getSeverityClass(log.severity);
const timestamp = new Date(log.timestamp).toLocaleString();
row.innerHTML = `
<td class="timestamp">${timestamp}</td>
<td class="event-type">${escapeHtml(log.event_type)}</td>
<td class="category">
<span class="category-badge ${log.event_category}">${log.event_category}</span>
</td>
<td class="description">${escapeHtml(log.description)}</td>
<td class="severity">
<span class="severity-badge ${log.severity.toLowerCase()}">${log.severity}</span>
</td>
<td class="username">${log.username ? escapeHtml(log.username) : '<em>System</em>'}</td>
<td class="ip-address">${log.ip_address || '-'}</td>
`;
tbody.appendChild(row);
});
showTable();
}
// 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';
}
}
function escapeHtml(text) {
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';
}
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';
}
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';
}
function showError(message) {
// Simple error display - you could enhance this with a proper toast/notification system
alert(message);
showEmpty();
}
// Update pagination
function updatePagination() {
const totalPages = Math.ceil(totalLogs / logsPerPage);
const info = document.getElementById('paginationInfo');
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;
}
// Pagination controls
function previousPage() {
if (currentPage > 1) {
currentPage--;
loadLogs();
}
}
function nextPage() {
currentPage++;
loadLogs();
}
// Refresh logs
function refreshLogs() {
loadLogs();
loadStats();
}
// Export logs
function exportLogs() {
const params = new URLSearchParams({
days: currentFilters.days,
format: 'csv'
});
window.open(`/api/logs/export?${params}`, '_blank');
}
// Cleanup modal functions
function cleanupLogs() {
document.getElementById('cleanupModal').style.display = 'flex';
}
function closeCleanupModal() {
document.getElementById('cleanupModal').style.display = 'none';
}
async function confirmCleanup() {
const daysToKeep = parseInt(document.getElementById('daysToKeep').value);
try {
const response = await fetch('/api/logs/cleanup', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
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.`);
closeCleanupModal();
loadLogs();
loadStats();
} else {
alert('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.');
}
}
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');
if (totalElement) totalElement.textContent = stats.total_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;
}
</script>
<style>
/* Admin Logs Styles */
.logs-page {
padding: var(--spacing-6);
max-width: 1400px;
margin: 0 auto;
}
.logs-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-8);
padding-bottom: var(--spacing-6);
border-bottom: 2px solid var(--gray-200);
}
.header-content h1 {
font-size: var(--font-size-3xl);
font-weight: 700;
color: var(--gray-900);
margin-bottom: var(--spacing-2);
display: flex;
align-items: center;
gap: var(--spacing-3);
}
.header-content p {
color: var(--gray-600);
font-size: var(--font-size-lg);
}
.back-button {
display: inline-flex;
align-items: center;
gap: var(--spacing-2);
color: var(--primary-color);
text-decoration: none;
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-4);
transition: var(--transition);
}
.back-button:hover {
color: var(--primary-hover);
}
.header-actions {
display: flex;
gap: var(--spacing-3);
}
/* Log Statistics */
.log-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-6);
margin-bottom: var(--spacing-8);
}
.stat-card {
background: var(--white);
padding: var(--spacing-6);
border-radius: var(--radius-xl);
box-shadow: var(--shadow);
display: flex;
align-items: center;
gap: var(--spacing-4);
transition: var(--transition);
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-lg);
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: var(--radius-lg);
display: flex;
align-items: center;
justify-content: center;
font-size: var(--font-size-xl);
color: var(--white);
background: linear-gradient(135deg, var(--primary-color), var(--primary-hover));
}
.stat-icon.security {
background: linear-gradient(135deg, var(--warning-color), #b45309);
}
.stat-icon.errors {
background: linear-gradient(135deg, var(--danger-color), #b91c1c);
}
.stat-icon.users {
background: linear-gradient(135deg, var(--success-color), #059669);
}
.stat-info h3 {
font-size: var(--font-size-2xl);
font-weight: 700;
color: var(--gray-900);
margin-bottom: var(--spacing-1);
}
.stat-info p {
color: var(--gray-500);
font-size: var(--font-size-sm);
}
/* Log Controls */
.log-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-6);
padding: var(--spacing-4);
background: var(--white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.search-filters {
display: flex;
align-items: center;
gap: var(--spacing-4);
flex: 1;
}
.search-box {
position: relative;
flex: 1;
max-width: 400px;
}
.search-box input {
width: 100%;
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);
}
.search-box i {
position: absolute;
left: var(--spacing-3);
top: 50%;
transform: translateY(-50%);
color: var(--gray-400);
}
.filter-group {
display: flex;
gap: var(--spacing-3);
}
.filter-select {
padding: var(--spacing-2) var(--spacing-3);
border: 1px solid var(--gray-300);
border-radius: var(--radius);
font-size: var(--font-size-sm);
background: var(--white);
}
/* Logs Table */
.logs-table-container {
background: var(--white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
overflow: hidden;
margin-bottom: var(--spacing-6);
}
.logs-table {
width: 100%;
border-collapse: collapse;
}
.logs-table th,
.logs-table td {
padding: var(--spacing-3) var(--spacing-4);
text-align: left;
border-bottom: 1px solid var(--gray-200);
}
.logs-table th {
background: var(--gray-50);
font-weight: 600;
color: var(--gray-700);
font-size: var(--font-size-sm);
}
.logs-table tr:hover {
background: var(--gray-50);
}
.logs-table tr.severity-high {
border-left: 4px solid var(--danger-color);
}
.logs-table tr.severity-medium {
border-left: 4px solid var(--warning-color);
}
.logs-table tr.severity-low {
border-left: 4px solid var(--success-color);
}
.logs-table tr.severity-info {
border-left: 4px solid var(--primary-color);
}
.timestamp {
font-family: var(--font-mono);
font-size: var(--font-size-xs);
color: var(--gray-600);
white-space: nowrap;
}
.event-type {
font-weight: 600;
color: var(--gray-900);
font-size: var(--font-size-sm);
}
.category-badge,
.severity-badge {
padding: var(--spacing-1) var(--spacing-2);
border-radius: var(--radius);
font-size: var(--font-size-xs);
font-weight: 600;
text-transform: uppercase;
}
.category-badge.security {
background: #fef3c7;
color: #92400e;
}
.category-badge.database {
background: #dbeafe;
color: #1e40af;
}
.category-badge.user_activity {
background: #d1fae5;
color: #065f46;
}
.category-badge.system {
background: #f3e8ff;
color: #7c3aed;
}
.severity-badge.high {
background: var(--danger-color);
color: var(--white);
}
.severity-badge.medium {
background: var(--warning-color);
color: var(--white);
}
.severity-badge.low {
background: var(--success-color);
color: var(--white);
}
.severity-badge.info {
background: var(--primary-color);
color: var(--white);
}
.description {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.username {
font-weight: 500;
color: var(--gray-700);
}
.ip-address {
font-family: var(--font-mono);
font-size: var(--font-size-xs);
color: var(--gray-500);
}
/* Loading and Empty States */
.loading-state,
.empty-state {
text-align: center;
padding: var(--spacing-12);
color: var(--gray-500);
}
.loading-state i {
font-size: var(--font-size-2xl);
margin-bottom: var(--spacing-4);
color: var(--primary-color);
}
.empty-state i {
font-size: 4rem;
margin-bottom: var(--spacing-4);
color: var(--gray-300);
}
.empty-state h3 {
font-size: var(--font-size-xl);
color: var(--gray-700);
margin-bottom: var(--spacing-2);
}
/* Pagination */
.pagination-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-4);
background: var(--white);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.pagination-info {
color: var(--gray-600);
font-size: var(--font-size-sm);
}
.pagination-controls {
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.pagination-controls button {
min-width: 80px;
}
.pagination-controls button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--white);
border-radius: var(--radius-lg);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-6);
border-bottom: 1px solid var(--gray-200);
}
.modal-header h3 {
margin: 0;
display: flex;
align-items: center;
gap: var(--spacing-2);
}
.modal-close {
background: none;
border: none;
font-size: var(--font-size-xl);
cursor: pointer;
color: var(--gray-400);
padding: var(--spacing-1);
}
.modal-close:hover {
color: var(--gray-600);
}
.modal-body {
padding: var(--spacing-6);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-3);
padding: var(--spacing-6);
border-top: 1px solid var(--gray-200);
}
.warning-note {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: var(--radius);
padding: var(--spacing-4);
margin-top: var(--spacing-4);
display: flex;
align-items: flex-start;
gap: var(--spacing-3);
}
.warning-note i {
color: #f59e0b;
margin-top: 2px;
}
/* Responsive Design */
@media (max-width: 768px) {
.logs-page {
padding: var(--spacing-4);
}
.logs-header {
flex-direction: column;
gap: var(--spacing-4);
}
.header-actions {
width: 100%;
justify-content: stretch;
}
.header-actions .btn {
flex: 1;
}
.log-stats {
grid-template-columns: 1fr;
}
.log-controls {
flex-direction: column;
gap: var(--spacing-4);
}
.search-filters {
flex-direction: column;
width: 100%;
}
.filter-group {
width: 100%;
}
.filter-select {
width: 100%;
}
.logs-table-container {
overflow-x: auto;
}
.logs-table {
min-width: 800px;
}
.pagination-wrapper {
flex-direction: column;
gap: var(--spacing-3);
}
.pagination-controls {
width: 100%;
justify-content: center;
}
}
@media (max-width: 480px) {
.stat-card {
flex-direction: column;
text-align: center;
gap: var(--spacing-3);
}
.stat-icon {
width: 50px;
height: 50px;
}
.modal-content {
width: 95%;
margin: var(--spacing-4);
}
.modal-footer {
flex-direction: column;
}
.modal-footer .btn {
width: 100%;
}
}
</style>
{% endblock %}