UPdate clear old logs by time
This commit is contained in:
@@ -2288,6 +2288,103 @@ def api_clear_logs():
|
|||||||
'error': f'Failed to clear logs: {str(e)}'
|
'error': f'Failed to clear logs: {str(e)}'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
@app.route('/api/logs/clear-old', methods=['POST'])
|
||||||
|
@admin_required
|
||||||
|
def api_clear_old_logs():
|
||||||
|
"""API endpoint to clear log entries older than specified days"""
|
||||||
|
try:
|
||||||
|
# Get JSON data
|
||||||
|
data = request.get_json()
|
||||||
|
if not data:
|
||||||
|
print("❌ No JSON data provided")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'No JSON data provided'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
days_threshold = data.get('days_threshold', 90)
|
||||||
|
admin_username = session.get('username', 'unknown')
|
||||||
|
print(f"🧹 Clear old logs request by admin: {admin_username}, threshold: {days_threshold} days")
|
||||||
|
|
||||||
|
# Validate input
|
||||||
|
if not isinstance(days_threshold, int) or days_threshold not in [30, 60, 90]:
|
||||||
|
print(f"❌ Invalid days_threshold: {days_threshold}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': 'days_threshold must be 30, 60, or 90'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Calculate cutoff date
|
||||||
|
cutoff_date = datetime.now() - timedelta(days=days_threshold)
|
||||||
|
|
||||||
|
# Count existing logs before deletion
|
||||||
|
try:
|
||||||
|
count_sql = "SELECT COUNT(*) as total_logs FROM log_events WHERE created_timestamp < :cutoff_date"
|
||||||
|
count_result = db.session.execute(text(count_sql), {'cutoff_date': cutoff_date}).fetchone()
|
||||||
|
total_logs = count_result.total_logs if count_result else 0
|
||||||
|
|
||||||
|
print(f"📊 Total logs older than {days_threshold} days to be cleared: {total_logs}")
|
||||||
|
|
||||||
|
if total_logs == 0:
|
||||||
|
print("✅ No old logs found to clear")
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'deleted_count': 0,
|
||||||
|
'message': f'No logs older than {days_threshold} days found to clear'
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as count_error:
|
||||||
|
print(f"⚠️ Error counting old logs: {count_error}")
|
||||||
|
total_logs = 0
|
||||||
|
|
||||||
|
# Perform the clear operation
|
||||||
|
try:
|
||||||
|
clear_sql = "DELETE FROM log_events WHERE created_timestamp < :cutoff_date"
|
||||||
|
result = db.session.execute(text(clear_sql), {'cutoff_date': cutoff_date})
|
||||||
|
deleted_count = result.rowcount
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
print(f"🗑️ Successfully cleared {deleted_count} log entries older than {days_threshold} days")
|
||||||
|
|
||||||
|
# Log the clear operation
|
||||||
|
logger_handler.log_security_event(
|
||||||
|
event_type="admin_clear_old_logs",
|
||||||
|
description=f"Admin {admin_username} cleared {deleted_count} log entries older than {days_threshold} days",
|
||||||
|
severity="HIGH",
|
||||||
|
additional_data={
|
||||||
|
'admin_user': admin_username,
|
||||||
|
'days_threshold': days_threshold,
|
||||||
|
'deleted_count': deleted_count,
|
||||||
|
'cutoff_date': cutoff_date.isoformat(),
|
||||||
|
'ip_address': request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'deleted_count': deleted_count,
|
||||||
|
'days_threshold': days_threshold,
|
||||||
|
'message': f'Successfully cleared {deleted_count} log entries older than {days_threshold} days',
|
||||||
|
'performed_by': admin_username,
|
||||||
|
'performed_at': datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as delete_error:
|
||||||
|
print(f"❌ Error during old log clearing: {delete_error}")
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Failed to clear old logs: {str(delete_error)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger_handler.log_database_error('api_clear_old_logs', e)
|
||||||
|
print(f"❌ Error in api_clear_old_logs: {e}")
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'error': f'Failed to clear old logs: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
@app.route('/api/logs/export')
|
@app.route('/api/logs/export')
|
||||||
@admin_required
|
@admin_required
|
||||||
def api_export_logs():
|
def api_export_logs():
|
||||||
|
|||||||
+458
-116
@@ -23,6 +23,10 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
Export
|
Export
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-warning" onclick="clearOldLogs()">
|
||||||
|
<i class="fas fa-broom"></i>
|
||||||
|
Clear Old Logs
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -193,11 +197,11 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Cleanup Modal -->
|
<!-- Log Details Modal -->
|
||||||
<div class="modal" id="cleanupModal">
|
<div class="modal" id="logDetailsModal">
|
||||||
<div class="modal" id="logDetailsModal">
|
<div class="modal-content">
|
||||||
<div class="modal-content log-details-modal">
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3><i class="fas fa-info-circle"></i> Log Entry Details</h3>
|
<h3><i class="fas fa-info-circle"></i> Log Entry Details</h3>
|
||||||
<button class="modal-close" onclick="closeLogDetailsModal()">
|
<button class="modal-close" onclick="closeLogDetailsModal()">
|
||||||
@@ -272,18 +276,19 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Cleanup Modal -->
|
||||||
|
<div class="modal" id="cleanupModal">
|
||||||
<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 class="modal-close" onclick="closeCleanupModal()">×</button>
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>
|
<p>
|
||||||
This action will permanently delete log entries older than the
|
This action will permanently delete log entries older than the specified
|
||||||
specified number of days.
|
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>
|
||||||
@@ -311,9 +316,50 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<!-- Clear Old Logs Modal -->
|
||||||
|
<div class="modal" id="clearOldLogsModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3><i class="fas fa-broom"></i> Clear Old Logs</h3>
|
||||||
|
<button class="modal-close" onclick="closeClearOldLogsModal()">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
This action will permanently delete log entries older than the selected
|
||||||
|
period.
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="clearOldLogsDays">Clear logs older than:</label>
|
||||||
|
<select id="clearOldLogsDays" class="form-control">
|
||||||
|
<option value="30">30 days</option>
|
||||||
|
<option value="60">60 days</option>
|
||||||
|
<option value="90" selected>90 days</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="warning-note">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Warning:</strong> This action cannot be undone. Log entries
|
||||||
|
older than the selected period will be permanently removed from the
|
||||||
|
system.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-secondary" onclick="closeClearOldLogsModal()">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" onclick="confirmClearOldLogs()">
|
||||||
|
<i class="fas fa-broom"></i>
|
||||||
|
Clear Old Logs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
// Global variables
|
// Global variables
|
||||||
let currentPage = 1;
|
let currentPage = 1;
|
||||||
let logsPerPage = 50;
|
let logsPerPage = 50;
|
||||||
@@ -481,9 +527,7 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
}</td>
|
}</td>
|
||||||
<td class="ip-address">${escapeHtml(log.ip_address || "-")}</td>
|
<td class="ip-address">${escapeHtml(log.ip_address || "-")}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
<button class="btn btn-sm btn-secondary" onclick="viewLogDetails(${JSON.stringify(
|
<button class="btn btn-sm btn-secondary" onclick="viewLogDetails(${index})">
|
||||||
log
|
|
||||||
).replace(/"/g, """)})">
|
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -492,6 +536,8 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Store logs globally for details modal
|
||||||
|
window.currentLogs = logs;
|
||||||
console.log(`📊 Displayed ${logs.length} log entries`);
|
console.log(`📊 Displayed ${logs.length} log entries`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,6 +559,7 @@ Management{% endblock %} {% block extra_head %}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export logs functionality
|
||||||
async function exportLogs() {
|
async function exportLogs() {
|
||||||
try {
|
try {
|
||||||
showMessage("Preparing log export...", "info");
|
showMessage("Preparing log export...", "info");
|
||||||
@@ -595,101 +642,13 @@ 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// View log details in modal
|
||||||
function viewLogDetails(logIndex) {
|
function viewLogDetails(logIndex) {
|
||||||
const log = window.currentLogs[logIndex];
|
const log = window.currentLogs[logIndex];
|
||||||
if (!log) return;
|
if (!log) return;
|
||||||
|
|
||||||
// Populate modal with log details
|
// Populate modal with log details
|
||||||
document.getElementById("detailEventId").textContent =
|
document.getElementById("detailEventId").textContent = log.event_id || "-";
|
||||||
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 =
|
||||||
@@ -965,6 +924,7 @@ ${
|
|||||||
loadStats();
|
loadStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear all logs functionality
|
||||||
async function clearLogs() {
|
async function clearLogs() {
|
||||||
// Show confirmation dialog
|
// Show confirmation dialog
|
||||||
if (
|
if (
|
||||||
@@ -1005,9 +965,7 @@ ${
|
|||||||
loadStats();
|
loadStats();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
showError(
|
showError("❌ Error clearing logs: " + (data.error || "Unknown error"));
|
||||||
"❌ Error clearing logs: " + (data.error || "Unknown error")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error clearing logs:", error);
|
console.error("Error clearing logs:", error);
|
||||||
@@ -1015,7 +973,7 @@ ${
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup logs
|
// Cleanup logs functionality
|
||||||
function cleanupLogs() {
|
function cleanupLogs() {
|
||||||
document.getElementById("cleanupModal").style.display = "flex";
|
document.getElementById("cleanupModal").style.display = "flex";
|
||||||
}
|
}
|
||||||
@@ -1055,9 +1013,7 @@ ${
|
|||||||
loadStats();
|
loadStats();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
showError(
|
showError("Error cleaning up logs: " + (data.error || "Unknown error"));
|
||||||
"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);
|
||||||
@@ -1065,6 +1021,68 @@ ${
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear old logs functionality
|
||||||
|
function clearOldLogs() {
|
||||||
|
console.log("clearOldLogs function called");
|
||||||
|
const modal = document.getElementById("clearOldLogsModal");
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = "flex";
|
||||||
|
modal.style.visibility = "visible";
|
||||||
|
modal.style.opacity = "1";
|
||||||
|
console.log("Modal should be visible now");
|
||||||
|
} else {
|
||||||
|
console.error("clearOldLogsModal not found!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeClearOldLogsModal() {
|
||||||
|
const modal = document.getElementById("clearOldLogsModal");
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = "none";
|
||||||
|
modal.style.visibility = "hidden";
|
||||||
|
modal.style.opacity = "0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmClearOldLogs() {
|
||||||
|
const daysThreshold = parseInt(
|
||||||
|
document.getElementById("clearOldLogsDays").value
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
showMessage("Clearing old logs...", "info");
|
||||||
|
|
||||||
|
const response = await fetch("/api/logs/clear-old", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ days_threshold: daysThreshold }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
showSuccess(
|
||||||
|
`Successfully cleared ${data.deleted_count} log entries older than ${daysThreshold} days`
|
||||||
|
);
|
||||||
|
closeClearOldLogsModal();
|
||||||
|
setTimeout(() => {
|
||||||
|
loadLogs();
|
||||||
|
loadStats();
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
showError(
|
||||||
|
"Error clearing old logs: " + (data.error || "Unknown error")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error clearing old logs:", error);
|
||||||
|
showError("Failed to clear old logs: " + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stats display
|
||||||
function updateStatsDisplay(stats) {
|
function updateStatsDisplay(stats) {
|
||||||
const totalElement = document.getElementById("totalEventsCount");
|
const totalElement = document.getElementById("totalEventsCount");
|
||||||
const securityElement = document.getElementById("securityEventsCount");
|
const securityElement = document.getElementById("securityEventsCount");
|
||||||
@@ -1079,9 +1097,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);
|
||||||
@@ -1133,6 +1151,109 @@ ${
|
|||||||
gap: var(--spacing-3);
|
gap: var(--spacing-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Button Styles */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
padding: var(--spacing-2) var(--spacing-4);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--gray-500);
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--gray-600);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: #f59e0b;
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning:hover {
|
||||||
|
background: #d97706;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: #ef4444;
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background: #0ea5e9;
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info:hover {
|
||||||
|
background: #0284c7;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: var(--spacing-1) var(--spacing-3);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Controls */
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--spacing-3);
|
||||||
|
border: 1px solid var(--gray-300);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: var(--spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: var(--spacing-2);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-700);
|
||||||
|
}
|
||||||
|
|
||||||
/* Log Statistics */
|
/* Log Statistics */
|
||||||
.log-stats {
|
.log-stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1430,11 +1551,58 @@ ${
|
|||||||
|
|
||||||
.pagination-controls button {
|
.pagination-controls button {
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
|
padding: var(--spacing-2) var(--spacing-3);
|
||||||
|
border: 1px solid var(--gray-300);
|
||||||
|
background: var(--white);
|
||||||
|
color: var(--gray-700);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-controls button:hover:not(:disabled) {
|
||||||
|
background: var(--gray-50);
|
||||||
|
border-color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-controls button:disabled {
|
.pagination-controls button:disabled {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
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 */
|
||||||
@@ -1449,6 +1617,7 @@ ${
|
|||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@@ -1458,6 +1627,19 @@ ${
|
|||||||
width: 90%;
|
width: 90%;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
animation: modalSlideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes modalSlideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50px) scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
@@ -1473,6 +1655,8 @@ ${
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-2);
|
gap: var(--spacing-2);
|
||||||
|
color: var(--gray-900);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close {
|
.modal-close {
|
||||||
@@ -1482,10 +1666,13 @@ ${
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--gray-400);
|
color: var(--gray-400);
|
||||||
padding: var(--spacing-1);
|
padding: var(--spacing-1);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
transition: var(--transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-close:hover {
|
.modal-close:hover {
|
||||||
color: var(--gray-600);
|
color: var(--gray-600);
|
||||||
|
background: var(--gray-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
@@ -1498,6 +1685,7 @@ ${
|
|||||||
gap: var(--spacing-3);
|
gap: var(--spacing-3);
|
||||||
padding: var(--spacing-6);
|
padding: var(--spacing-6);
|
||||||
border-top: 1px solid var(--gray-200);
|
border-top: 1px solid var(--gray-200);
|
||||||
|
background: var(--gray-50);
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning-note {
|
.warning-note {
|
||||||
@@ -1514,6 +1702,74 @@ ${
|
|||||||
.warning-note i {
|
.warning-note i {
|
||||||
color: #f59e0b;
|
color: #f59e0b;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning-note strong {
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Log Details Modal Specific Styles */
|
||||||
|
.log-details-modal {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section {
|
||||||
|
margin-bottom: var(--spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-section h4 {
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-900);
|
||||||
|
margin-bottom: var(--spacing-3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-2) 0;
|
||||||
|
border-bottom: 1px solid var(--gray-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--gray-700);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
color: var(--gray-900);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-box,
|
||||||
|
.json-box {
|
||||||
|
background: var(--gray-50);
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: var(--spacing-4);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-details-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: var(--spacing-6);
|
||||||
|
margin-bottom: var(--spacing-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@@ -1575,6 +1831,10 @@ ${
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-details-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
@@ -1600,8 +1860,90 @@ ${
|
|||||||
|
|
||||||
.modal-footer .btn {
|
.modal-footer .btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
{% endblock %}
|
/* Enhanced Animations */
|
||||||
</div>
|
.stat-card {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table tbody tr {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus and accessibility improvements */
|
||||||
|
.btn:focus,
|
||||||
|
.filter-select:focus,
|
||||||
|
.search-box input:focus {
|
||||||
|
outline: 2px solid var(--primary-color);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print styles */
|
||||||
|
@media print {
|
||||||
|
.logs-header .header-actions,
|
||||||
|
.log-controls,
|
||||||
|
.pagination-wrapper,
|
||||||
|
.modal {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-page {
|
||||||
|
padding: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-table th,
|
||||||
|
.logs-table td {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user