UPdate clear old logs by time

This commit is contained in:
2025-08-17 11:32:22 -04:00
parent 7114ed24c9
commit 8ae60bb48a
2 changed files with 1710 additions and 1271 deletions
+97
View File
@@ -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():
+450 -108
View File
@@ -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 log-details-modal"> <div class="modal-content">
<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()">
@@ -273,17 +277,18 @@ Management{% endblock %} {% block extra_head %}
</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()">&times;</button>
&times;
</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>
@@ -313,6 +318,47 @@ Management{% endblock %} {% block extra_head %}
</div> </div>
</div> </div>
<!-- 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()">
&times;
</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> <script>
// Global variables // Global variables
let currentPage = 1; let currentPage = 1;
@@ -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, "&quot;")})">
<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");
@@ -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;
}
}
/* Enhanced Animations */
.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> </style>
{% endblock %} {% endblock %}
</div>