You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
539 lines
20 KiB
539 lines
20 KiB
{% extends "base.html" %}
|
|
|
|
{% block title %}System Logs - Plutus{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
/* Background styling */
|
|
body {
|
|
background-color: #3a3a3a !important;
|
|
background-image: url("{{ url_for('static', filename='images/plutus3.JPG') }}") !important;
|
|
background-size: cover;
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-attachment: fixed;
|
|
position: relative;
|
|
}
|
|
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: rgba(58, 58, 58, 0.85);
|
|
z-index: -1;
|
|
}
|
|
|
|
.container-fluid, .container {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Ensure navbar and footer are above background */
|
|
.navbar, .footer, main {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Page title and breadcrumb styling */
|
|
h1, h2, h3, h4, h5, h6 {
|
|
color: #faf8f0 !important;
|
|
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
|
}
|
|
|
|
.breadcrumb-item a {
|
|
color: #d4af37 !important;
|
|
}
|
|
|
|
.breadcrumb-item.active {
|
|
color: #faf8f0 !important;
|
|
}
|
|
|
|
.card {
|
|
background-color: rgba(250, 248, 240, 0.98) !important;
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.table-responsive {
|
|
background-color: rgba(250, 248, 240, 0.98);
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.alert {
|
|
background-color: rgba(250, 248, 240, 0.98);
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{{ url_for('main.index') }}">Dashboard</a></li>
|
|
<li class="breadcrumb-item active" aria-current="page">System Logs</li>
|
|
</ol>
|
|
</nav>
|
|
|
|
<div class="d-flex justify-content-between align-items-start mb-4">
|
|
<div>
|
|
<h1 class="h2 mb-1">System Logs</h1>
|
|
<p class="text-muted">User activity and system audit trail</p>
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-info" onclick="exportLogs()">
|
|
<i class="fas fa-download"></i> Export Logs
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Controls -->
|
|
<div class="card shadow mb-4">
|
|
<div class="card-body">
|
|
<h2 class="h5 mb-3">
|
|
<i class="fas fa-filter"></i> Filters
|
|
</h2>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-6 col-lg-4">
|
|
<label class="form-label form-label-sm">Search:</label>
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
|
<input class="form-control" type="text" id="searchInput" placeholder="Search logs, actions, details...">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6 col-lg-2">
|
|
<label class="form-label form-label-sm">User:</label>
|
|
<select class="form-select form-select-sm" id="userFilter">
|
|
<option value="">All Users</option>
|
|
{% for user in users %}
|
|
<option value="{{ user.id }}">{{ user.FullName }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-6 col-lg-2">
|
|
<label class="form-label form-label-sm">Action:</label>
|
|
<select class="form-select form-select-sm" id="actionFilter">
|
|
<option value="">All Actions</option>
|
|
{% for action in actions %}
|
|
<option value="{{ action }}">{{ action }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-6 col-lg-2">
|
|
<label class="form-label form-label-sm">Entity Type:</label>
|
|
<select class="form-select form-select-sm" id="entityTypeFilter">
|
|
<option value="">All Types</option>
|
|
{% for entity_type in entity_types %}
|
|
<option value="{{ entity_type }}">{{ entity_type }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-6 col-lg-3">
|
|
<label class="form-label form-label-sm">Date From:</label>
|
|
<input class="form-control form-control-sm" type="date" id="dateFromFilter">
|
|
</div>
|
|
|
|
<div class="col-md-6 col-lg-3">
|
|
<label class="form-label form-label-sm">Date To:</label>
|
|
<input class="form-control form-control-sm" type="date" id="dateToFilter">
|
|
</div>
|
|
|
|
<div class="col-md-6 col-lg-3 d-flex align-items-end">
|
|
<button class="btn btn-sm btn-info me-2" onclick="applyFilters()">
|
|
<i class="fas fa-search"></i> Apply Filters
|
|
</button>
|
|
<button class="btn btn-sm btn-light" onclick="clearFilters()">
|
|
<i class="fas fa-times"></i> Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Summary -->
|
|
<div class="alert alert-info" id="filterResults" style="display: none;">
|
|
<span id="resultCount">0</span> of {{ logs|length }} log entries shown
|
|
</div>
|
|
|
|
<!-- Logs Table -->
|
|
<div class="card shadow">
|
|
<div class="card-body">
|
|
<h2 class="h5 mb-3">
|
|
<i class="fas fa-list"></i> Log Entries
|
|
</h2>
|
|
|
|
{% if logs %}
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover" id="logsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Timestamp</th>
|
|
<th>User</th>
|
|
<th>Action</th>
|
|
<th>Entity</th>
|
|
<th>Details</th>
|
|
<th>IP Address</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="logsTableBody">
|
|
{% for log in logs %}
|
|
<tr>
|
|
<td>
|
|
<small class="d-block">{{ log.Added.strftime('%Y-%m-%d') }}</small>
|
|
<small class="text-muted">{{ log.Added.strftime('%H:%M:%S') }}</small>
|
|
</td>
|
|
<td>
|
|
<div>
|
|
<strong>{{ log.user_name or 'System' }}</strong>
|
|
{% if log.User_ID %}
|
|
<br><small class="text-muted">ID: {{ log.User_ID }}</small>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{% if log.Action %}
|
|
<span class="badge bg-info">{{ log.Action }}</span>
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if log.Entity_Type %}
|
|
<div>
|
|
<span class="badge bg-primary">{{ log.Entity_Type }}</span>
|
|
{% if log.Entity_ID %}
|
|
<br><small class="text-muted">ID: {{ log.Entity_ID }}</small>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if log.Log_Entry %}
|
|
<small>
|
|
{{ log.Log_Entry[:100] }}{% if log.Log_Entry|length > 100 %}...{% endif %}
|
|
</small>
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if log.IP_Address %}
|
|
<code class="small">{{ log.IP_Address }}</code>
|
|
{% else %}
|
|
<span class="text-muted">-</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-info btn-sm" onclick="showLogDetail({{ log.id }})">
|
|
<i class="fas fa-eye"></i> View
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if pagination %}
|
|
<nav aria-label="Logs pagination">
|
|
<ul class="pagination justify-content-center mt-3">
|
|
<li class="page-item {% if not pagination.has_prev %}disabled{% endif %}">
|
|
<a class="page-link" href="{% if pagination.has_prev %}{{ url_for('main.logs_list', page=pagination.prev_num, **request.args) }}{% else %}#{% endif %}">Previous</a>
|
|
</li>
|
|
|
|
{% for page_num in pagination.iter_pages() %}
|
|
{% if page_num %}
|
|
<li class="page-item {% if page_num == pagination.page %}active{% endif %}">
|
|
<a class="page-link" href="{{ url_for('main.logs_list', page=page_num, **request.args) }}">{{ page_num }}</a>
|
|
</li>
|
|
{% else %}
|
|
<li class="page-item disabled">
|
|
<span class="page-link">...</span>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
<li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
|
|
<a class="page-link" href="{% if pagination.has_next %}{{ url_for('main.logs_list', page=pagination.next_num, **request.args) }}{% else %}#{% endif %}">Next</a>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<div class="alert alert-info">
|
|
<p class="mb-0">No log entries found.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Log Detail Modal -->
|
|
<div class="modal fade" id="logDetailModal" tabindex="-1" aria-labelledby="logDetailModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="logDetailModalLabel">
|
|
<i class="fas fa-file-alt"></i> Log Entry Details
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="logDetailContent">
|
|
<!-- Log details will be populated here -->
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-info" onclick="copyLogDetails()">
|
|
<i class="fas fa-copy"></i> Copy Details
|
|
</button>
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let allLogs = [];
|
|
let filteredLogs = [];
|
|
|
|
// Initialize logs and filters when page loads
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeLogs();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function initializeLogs() {
|
|
const tableBody = document.getElementById('logsTableBody');
|
|
const rows = tableBody.querySelectorAll('tr');
|
|
|
|
allLogs = Array.from(rows).map(row => {
|
|
const cells = row.querySelectorAll('td');
|
|
return {
|
|
element: row,
|
|
timestamp: cells[0] ? (cells[0].textContent.trim() || '') : '',
|
|
user: cells[1] ? (cells[1].textContent.trim() || '') : '',
|
|
action: cells[2] ? (cells[2].textContent.trim() || '') : '',
|
|
entityType: cells[3] ? (cells[3].textContent.trim() || '') : '',
|
|
details: cells[4] ? (cells[4].textContent.trim() || '') : '',
|
|
ipAddress: cells[5] ? (cells[5].textContent.trim() || '') : ''
|
|
};
|
|
});
|
|
|
|
filteredLogs = [...allLogs];
|
|
updateResultCount();
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
document.getElementById('searchInput').addEventListener('input', applyFilters);
|
|
document.getElementById('userFilter').addEventListener('change', applyFilters);
|
|
document.getElementById('actionFilter').addEventListener('change', applyFilters);
|
|
document.getElementById('entityTypeFilter').addEventListener('change', applyFilters);
|
|
document.getElementById('dateFromFilter').addEventListener('change', applyFilters);
|
|
document.getElementById('dateToFilter').addEventListener('change', applyFilters);
|
|
}
|
|
|
|
function applyFilters() {
|
|
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
|
const userFilter = document.getElementById('userFilter').value;
|
|
const actionFilter = document.getElementById('actionFilter').value;
|
|
const entityTypeFilter = document.getElementById('entityTypeFilter').value;
|
|
const dateFromFilter = document.getElementById('dateFromFilter').value;
|
|
const dateToFilter = document.getElementById('dateToFilter').value;
|
|
|
|
// Filter logs
|
|
filteredLogs = allLogs.filter(log => {
|
|
// Search filter
|
|
const searchMatch = !searchTerm ||
|
|
log.user.toLowerCase().includes(searchTerm) ||
|
|
log.action.toLowerCase().includes(searchTerm) ||
|
|
log.entityType.toLowerCase().includes(searchTerm) ||
|
|
log.details.toLowerCase().includes(searchTerm) ||
|
|
log.ipAddress.toLowerCase().includes(searchTerm);
|
|
|
|
// User filter
|
|
const userMatch = !userFilter || log.user.includes(`ID: ${userFilter}`);
|
|
|
|
// Action filter
|
|
const actionMatch = !actionFilter || log.action.includes(actionFilter);
|
|
|
|
// Entity type filter
|
|
const entityTypeMatch = !entityTypeFilter || log.entityType.includes(entityTypeFilter);
|
|
|
|
// Date filters would need server-side implementation for full functionality
|
|
// For now, we'll implement basic client-side date filtering on visible text
|
|
let dateMatch = true;
|
|
if (dateFromFilter || dateToFilter) {
|
|
const logDate = log.timestamp.split(' ')[0]; // Get just the date part
|
|
if (dateFromFilter && logDate < dateFromFilter) dateMatch = false;
|
|
if (dateToFilter && logDate > dateToFilter) dateMatch = false;
|
|
}
|
|
|
|
return searchMatch && userMatch && actionMatch && entityTypeMatch && dateMatch;
|
|
});
|
|
|
|
// Update display
|
|
updateTable();
|
|
updateResultCount();
|
|
}
|
|
|
|
function updateTable() {
|
|
const tableBody = document.getElementById('logsTableBody');
|
|
|
|
// Hide all rows first
|
|
allLogs.forEach(log => {
|
|
log.element.style.display = 'none';
|
|
});
|
|
|
|
// Show filtered rows
|
|
filteredLogs.forEach(log => {
|
|
log.element.style.display = '';
|
|
tableBody.appendChild(log.element); // Re-append to maintain order
|
|
});
|
|
}
|
|
|
|
function updateResultCount() {
|
|
const resultCount = document.getElementById('resultCount');
|
|
const filterResults = document.getElementById('filterResults');
|
|
|
|
resultCount.textContent = filteredLogs.length;
|
|
|
|
if (filteredLogs.length === allLogs.length) {
|
|
filterResults.style.display = 'none';
|
|
} else {
|
|
filterResults.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function clearFilters() {
|
|
document.getElementById('searchInput').value = '';
|
|
document.getElementById('userFilter').value = '';
|
|
document.getElementById('actionFilter').value = '';
|
|
document.getElementById('entityTypeFilter').value = '';
|
|
document.getElementById('dateFromFilter').value = '';
|
|
document.getElementById('dateToFilter').value = '';
|
|
applyFilters();
|
|
}
|
|
|
|
function showLogDetail(logId) {
|
|
// Fetch log details via AJAX
|
|
fetch(`/logs/detail/${logId}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
const log = data.log;
|
|
const detailHtml = `
|
|
<div>
|
|
<table class="table table-bordered">
|
|
<tbody>
|
|
<tr>
|
|
<td><strong>ID</strong></td>
|
|
<td>#${log.id}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Timestamp</strong></td>
|
|
<td>${log.timestamp}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>User</strong></td>
|
|
<td>${log.user_name || 'System'} ${log.User_ID ? `(ID: ${log.User_ID})` : ''}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Action</strong></td>
|
|
<td>${log.Action || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Entity Type</strong></td>
|
|
<td>${log.Entity_Type || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Entity ID</strong></td>
|
|
<td>${log.Entity_ID || '-'}</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>IP Address</strong></td>
|
|
<td>${log.IP_Address || '-'}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
${log.Log_Entry ? `
|
|
<div class="mb-3">
|
|
<label class="form-label"><strong>Full Details:</strong></label>
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<pre class="mb-0">${log.Log_Entry}</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
` : ''}
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('logDetailContent').innerHTML = detailHtml;
|
|
const modal = new bootstrap.Modal(document.getElementById('logDetailModal'));
|
|
modal.show();
|
|
} else {
|
|
alert('Failed to load log details: ' + data.error);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching log details:', error);
|
|
alert('Failed to load log details. Please try again.');
|
|
});
|
|
}
|
|
|
|
function copyLogDetails() {
|
|
const content = document.getElementById('logDetailContent').innerText;
|
|
navigator.clipboard.writeText(content).then(function() {
|
|
// Show temporary success message
|
|
const button = event.target.closest('button');
|
|
const originalText = button.innerHTML;
|
|
button.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
|
button.classList.remove('btn-info');
|
|
button.classList.add('btn-success');
|
|
|
|
setTimeout(function() {
|
|
button.innerHTML = originalText;
|
|
button.classList.remove('btn-success');
|
|
button.classList.add('btn-info');
|
|
}, 2000);
|
|
}).catch(function(err) {
|
|
console.error('Failed to copy text: ', err);
|
|
alert('Failed to copy to clipboard');
|
|
});
|
|
}
|
|
|
|
function exportLogs() {
|
|
// Create form data with current filters
|
|
const params = new URLSearchParams();
|
|
|
|
const searchTerm = document.getElementById('searchInput').value;
|
|
const userFilter = document.getElementById('userFilter').value;
|
|
const actionFilter = document.getElementById('actionFilter').value;
|
|
const entityTypeFilter = document.getElementById('entityTypeFilter').value;
|
|
const dateFromFilter = document.getElementById('dateFromFilter').value;
|
|
const dateToFilter = document.getElementById('dateToFilter').value;
|
|
|
|
if (searchTerm) params.append('search', searchTerm);
|
|
if (userFilter) params.append('user', userFilter);
|
|
if (actionFilter) params.append('action', actionFilter);
|
|
if (entityTypeFilter) params.append('entity_type', entityTypeFilter);
|
|
if (dateFromFilter) params.append('date_from', dateFromFilter);
|
|
if (dateToFilter) params.append('date_to', dateToFilter);
|
|
|
|
// Open export URL in new window
|
|
window.open(`/logs/export?${params.toString()}`, '_blank');
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|