Files
GOV_QR_Codes_Management/templates/admin_logs.html
T
2025-08-12 21:27:59 -04:00

1354 lines
35 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="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
</button>
<button class="btn btn-warning" onclick="cleanupLogs()">
<i class="fas fa-trash-alt"></i>
Cleanup Old Logs
</button>
</div>
</div>
<!-- Log Statistics -->
<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 id="totalEventsCount">
{{ log_stats.total_events if log_stats else 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 id="securityEventsCount">
{{ log_stats.security_events if log_stats else 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 id="databaseErrorsCount">
{{ log_stats.database_errors if log_stats else 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 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 -->
<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>
<th>Actions</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" 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>
<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 () {
console.log("Admin logs page loading...");
loadLogs();
setupEventListeners();
});
// Setup event listeners
function setupEventListeners() {
// Search input
const searchInput = document.getElementById("searchLogs");
if (searchInput) {
let searchTimeout;
searchInput.addEventListener("input", function () {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
currentFilters.search = this.value;
currentPage = 1;
loadLogs();
}, 500);
});
}
// Filter selects
const categoryFilter = document.getElementById("categoryFilter");
if (categoryFilter) {
categoryFilter.addEventListener("change", function () {
currentFilters.category = this.value;
currentPage = 1;
loadLogs();
});
}
const severityFilter = document.getElementById("severityFilter");
if (severityFilter) {
severityFilter.addEventListener("change", function () {
currentFilters.severity = this.value;
currentPage = 1;
loadLogs();
});
}
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,
});
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) {
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: " + error.message);
}
}
// 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);
} else {
console.error("Failed to load stats:", data.error);
}
} 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, 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">${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>
`;
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 || "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");
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) {
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");
if (info) {
const start = (currentPage - 1) * logsPerPage + 1;
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");
if (prevBtn) prevBtn.disabled = currentPage <= 1;
if (nextBtn) nextBtn.disabled = currentPage >= totalPages;
}
// Pagination controls
function previousPage() {
if (currentPage > 1) {
currentPage--;
loadLogs();
}
}
function nextPage() {
currentPage++;
loadLogs();
}
// Refresh logs
function refreshLogs() {
loadLogs();
loadStats();
}
// Test logging functionality
async function testLogging() {
try {
showMessage("Testing logging system...", "info");
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 logs
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);
if (daysToKeep < 7) {
showError("Cannot keep logs for less than 7 days");
return;
}
try {
showMessage("Cleaning up old logs...", "info");
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) {
showSuccess(
`Successfully cleaned up ${data.deleted_count} old log entries`
);
closeCleanupModal();
setTimeout(() => {
loadLogs();
loadStats();
}, 1000);
} else {
showError("Error cleaning up logs: " + (data.error || "Unknown error"));
}
} catch (error) {
console.error("Error cleaning up logs:", error);
showError("Failed to cleanup logs. Please try again.");
}
}
function updateStatsDisplay(stats) {
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 (errorsElement) errorsElement.textContent = stats.database_errors || 0;
if (usersElement) usersElement.textContent = stats.user_activities || 0;
console.log("Stats updated:", stats);
}
</script>
<style>
/* Admin Logs Styles */
.logs-page {
padding: var(--spacing-6);
max-width: 1600px;
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 %}