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.
 
 
 

510 lines
20 KiB

{% extends "base.html" %}
{% block title %}System Logs - Plutus{% endblock %}
{% block content %}
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><a href="{{ url_for('main.index') }}">Dashboard</a></li>
<li class="is-active"><a href="#" aria-current="page">System Logs</a></li>
</ul>
</nav>
<div class="level">
<div class="level-left">
<div>
<h1 class="title">System Logs</h1>
<p class="subtitle">User activity and system audit trail</p>
</div>
</div>
<div class="level-right">
<div class="field is-grouped">
<div class="control">
<button class="button is-info" onclick="exportLogs()">
<span class="icon"><i class="fas fa-download"></i></span>
<span>Export Logs</span>
</button>
</div>
</div>
</div>
</div>
<!-- Filter Controls -->
<div class="box">
<h2 class="title is-5">
<span class="icon"><i class="fas fa-filter"></i></span>
Filters
</h2>
<div class="field is-grouped is-grouped-multiline">
<div class="control">
<label class="label is-small">Search:</label>
<div class="field has-addons">
<div class="control has-icons-left is-expanded">
<input class="input" type="text" id="searchInput" placeholder="Search logs, actions, details...">
<span class="icon is-small is-left">
<i class="fas fa-search"></i>
</span>
</div>
</div>
</div>
<div class="control">
<label class="label is-small">User:</label>
<div class="select">
<select id="userFilter">
<option value="">All Users</option>
{% for user in users %}
<option value="{{ user.id }}">{{ user.FullName }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control">
<label class="label is-small">Action:</label>
<div class="select">
<select id="actionFilter">
<option value="">All Actions</option>
{% for action in actions %}
<option value="{{ action }}">{{ action }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control">
<label class="label is-small">Entity Type:</label>
<div class="select">
<select id="entityTypeFilter">
<option value="">All Types</option>
{% for entity_type in entity_types %}
<option value="{{ entity_type }}">{{ entity_type }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="control">
<label class="label is-small">Date From:</label>
<input class="input" type="date" id="dateFromFilter">
</div>
<div class="control">
<label class="label is-small">Date To:</label>
<input class="input" type="date" id="dateToFilter">
</div>
<div class="control">
<button class="button is-small is-info" onclick="applyFilters()">
<span class="icon"><i class="fas fa-search"></i></span>
<span>Apply Filters</span>
</button>
</div>
<div class="control">
<button class="button is-small is-light" onclick="clearFilters()">
<span class="icon"><i class="fas fa-times"></i></span>
<span>Clear</span>
</button>
</div>
</div>
</div>
<!-- Results Summary -->
<div class="notification is-info is-light" id="filterResults" style="display: none;">
<span id="resultCount">0</span> of {{ logs|length }} log entries shown
</div>
<!-- Logs Table -->
<div class="box">
<h2 class="title is-5">
<span class="icon"><i class="fas fa-list"></i></span>
Log Entries
</h2>
{% if logs %}
<div class="table-container">
<table class="table is-fullwidth is-striped is-hoverable" 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>
<span class="is-size-7">{{ log.Added.strftime('%Y-%m-%d') }}</span><br>
<span class="is-size-7 has-text-grey">{{ log.Added.strftime('%H:%M:%S') }}</span>
</td>
<td>
<div class="media">
<div class="media-content">
<strong>{{ log.user_name or 'System' }}</strong>
{% if log.User_ID %}
<br><small class="has-text-grey">ID: {{ log.User_ID }}</small>
{% endif %}
</div>
</div>
</td>
<td>
{% if log.Action %}
<span class="tag is-info is-light">{{ log.Action }}</span>
{% else %}
<span class="has-text-grey">-</span>
{% endif %}
</td>
<td>
{% if log.Entity_Type %}
<div>
<span class="tag is-primary is-light">{{ log.Entity_Type }}</span>
{% if log.Entity_ID %}
<br><small class="has-text-grey">ID: {{ log.Entity_ID }}</small>
{% endif %}
</div>
{% else %}
<span class="has-text-grey">-</span>
{% endif %}
</td>
<td>
{% if log.Log_Entry %}
<div class="content is-small">
{{ log.Log_Entry[:100] }}{% if log.Log_Entry|length > 100 %}...{% endif %}
</div>
{% else %}
<span class="has-text-grey">-</span>
{% endif %}
</td>
<td>
{% if log.IP_Address %}
<code class="is-small">{{ log.IP_Address }}</code>
{% else %}
<span class="has-text-grey">-</span>
{% endif %}
</td>
<td>
<div class="buttons are-small">
<button class="button is-info is-outlined" onclick="showLogDetail({{ log.id }})">
<span class="icon"><i class="fas fa-eye"></i></span>
<span>View</span>
</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if pagination %}
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
{% if pagination.has_prev %}
<a class="pagination-previous" href="{{ url_for('main.logs_list', page=pagination.prev_num, **request.args) }}">Previous</a>
{% else %}
<a class="pagination-previous" disabled>Previous</a>
{% endif %}
{% if pagination.has_next %}
<a class="pagination-next" href="{{ url_for('main.logs_list', page=pagination.next_num, **request.args) }}">Next page</a>
{% else %}
<a class="pagination-next" disabled>Next page</a>
{% endif %}
<ul class="pagination-list">
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
{% if page_num != pagination.page %}
<li><a class="pagination-link" href="{{ url_for('main.logs_list', page=page_num, **request.args) }}">{{ page_num }}</a></li>
{% else %}
<li><a class="pagination-link is-current" href="#">{{ page_num }}</a></li>
{% endif %}
{% else %}
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% endif %}
{% endfor %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="notification is-info">
<p>No log entries found.</p>
</div>
{% endif %}
</div>
<!-- Log Detail Modal -->
<div class="modal" id="logDetailModal">
<div class="modal-background" onclick="hideModal('logDetailModal')"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">
<span class="icon"><i class="fas fa-file-alt"></i></span>
Log Entry Details
</p>
<button class="delete" aria-label="close" onclick="hideModal('logDetailModal')"></button>
</header>
<section class="modal-card-body">
<div id="logDetailContent">
<!-- Log details will be populated here -->
</div>
</section>
<footer class="modal-card-foot">
<button class="button is-info" onclick="copyLogDetails()">
<span class="icon"><i class="fas fa-copy"></i></span>
<span>Copy Details</span>
</button>
<button class="button" onclick="hideModal('logDetailModal')">Close</button>
</footer>
</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 class="content">
<table class="table is-fullwidth">
<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="field">
<label class="label">Full Details:</label>
<div class="box">
<pre class="has-text-dark">${log.Log_Entry}</pre>
</div>
</div>
` : ''}
</div>
`;
document.getElementById('logDetailContent').innerHTML = detailHtml;
document.getElementById('logDetailModal').classList.add('is-active');
} 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 hideModal(modalId) {
document.getElementById(modalId).classList.remove('is-active');
}
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 = '<span class="icon"><i class="fas fa-check"></i></span><span>Copied!</span>';
button.classList.add('is-success');
setTimeout(function() {
button.innerHTML = originalText;
button.classList.remove('is-success');
}, 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');
}
// Close modal on Escape key
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
const activeModals = document.querySelectorAll('.modal.is-active');
activeModals.forEach(modal => modal.classList.remove('is-active'));
}
});
</script>
{% endblock %}