440 lines
14 KiB
HTML
440 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Log di Sistema - Proxmox Manager{% endblock %}
|
|
|
|
{% block extra_styles %}
|
|
<style>
|
|
.logs-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--space-lg);
|
|
flex-wrap: wrap;
|
|
gap: var(--space-md);
|
|
}
|
|
|
|
.logs-filters {
|
|
display: flex;
|
|
gap: var(--space-md);
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-xs);
|
|
}
|
|
|
|
.filter-group label {
|
|
font-size: 0.85rem;
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.filter-group select,
|
|
.filter-group input {
|
|
padding: var(--space-sm) var(--space-md);
|
|
border: 1px solid var(--border-default);
|
|
border-radius: var(--radius-md);
|
|
background: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.logs-table-container {
|
|
overflow-x: auto;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-default);
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
|
|
.logs-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.logs-table thead {
|
|
background: var(--bg-tertiary);
|
|
border-bottom: 2px solid var(--border-default);
|
|
}
|
|
|
|
.logs-table th {
|
|
padding: var(--space-md);
|
|
text-align: left;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
font-size: 0.85rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.logs-table tbody tr {
|
|
border-bottom: 1px solid var(--border-default);
|
|
transition: background var(--transition-fast);
|
|
}
|
|
|
|
.logs-table tbody tr:hover {
|
|
background: var(--bg-tertiary);
|
|
}
|
|
|
|
.logs-table td {
|
|
padding: var(--space-md);
|
|
color: var(--text-primary);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.log-status {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: var(--space-xs) var(--space-sm);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 0.8rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.log-status.success {
|
|
background: rgba(63, 185, 80, 0.15);
|
|
color: var(--accent-green);
|
|
}
|
|
|
|
.log-status.failed {
|
|
background: rgba(255, 78, 80, 0.15);
|
|
color: var(--accent-red);
|
|
}
|
|
|
|
.log-action-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--space-xs);
|
|
padding: var(--space-xs) var(--space-sm);
|
|
background: rgba(88, 166, 255, 0.15);
|
|
color: var(--accent-blue);
|
|
border-radius: var(--radius-sm);
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.log-error-message {
|
|
color: var(--accent-red);
|
|
font-size: 0.85rem;
|
|
font-style: italic;
|
|
margin-top: var(--space-xs);
|
|
}
|
|
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: var(--space-md);
|
|
margin-top: var(--space-lg);
|
|
}
|
|
|
|
.pagination button {
|
|
padding: var(--space-sm) var(--space-md);
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-default);
|
|
border-radius: var(--radius-md);
|
|
color: var(--text-primary);
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.pagination button:hover:not(:disabled) {
|
|
background: var(--accent-blue);
|
|
color: white;
|
|
border-color: var(--accent-blue);
|
|
}
|
|
|
|
.pagination button:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.pagination-info {
|
|
color: var(--text-secondary);
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.export-btn {
|
|
padding: var(--space-sm) var(--space-md);
|
|
background: var(--accent-blue);
|
|
color: white;
|
|
border: none;
|
|
border-radius: var(--radius-md);
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all var(--transition-fast);
|
|
}
|
|
|
|
.export-btn:hover {
|
|
background: var(--accent-blue-hover);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.no-logs {
|
|
text-align: center;
|
|
padding: var(--space-xl);
|
|
color: var(--text-tertiary);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="logs-header">
|
|
<div>
|
|
<h1>Log di Sistema</h1>
|
|
<p class="subtitle">Visualizza tutte le attività del sistema</p>
|
|
</div>
|
|
<button class="export-btn" onclick="exportLogs()">
|
|
<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16" style="margin-right: 8px; vertical-align: middle;">
|
|
<path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
|
|
<path d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708l3-3z"/>
|
|
</svg>
|
|
Esporta CSV
|
|
</button>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="logs-filters">
|
|
<div class="filter-group">
|
|
<label>Utente</label>
|
|
<select id="filterUser" onchange="filterLogs()">
|
|
<option value="">Tutti gli utenti</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label>Azione</label>
|
|
<select id="filterAction" onchange="filterLogs()">
|
|
<option value="">Tutte le azioni</option>
|
|
<option value="start">Start</option>
|
|
<option value="stop">Stop</option>
|
|
<option value="restart">Restart</option>
|
|
<option value="shutdown">Shutdown</option>
|
|
<option value="snapshot">Snapshot</option>
|
|
<option value="backup">Backup</option>
|
|
<option value="login">Login</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label>Stato</label>
|
|
<select id="filterStatus" onchange="filterLogs()">
|
|
<option value="">Tutti gli stati</option>
|
|
<option value="success">Successo</option>
|
|
<option value="failed">Fallito</option>
|
|
</select>
|
|
</div>
|
|
<div class="filter-group">
|
|
<label>VM ID</label>
|
|
<input type="number" id="filterVmId" placeholder="ID VM" onchange="filterLogs()" min="0">
|
|
</div>
|
|
<div class="filter-group">
|
|
<label>Data</label>
|
|
<input type="date" id="filterDate" onchange="filterLogs()">
|
|
</div>
|
|
<div class="filter-group">
|
|
<label>Limiti</label>
|
|
<select id="filterLimit" onchange="filterLogs()">
|
|
<option value="50">50 record</option>
|
|
<option value="100" selected>100 record</option>
|
|
<option value="200">200 record</option>
|
|
<option value="500">500 record</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="logs-table-container">
|
|
<table class="logs-table">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Data/Ora</th>
|
|
<th>Utente</th>
|
|
<th>Azione</th>
|
|
<th>VM ID</th>
|
|
<th>Stato</th>
|
|
<th>IP</th>
|
|
<th>Dettagli</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="logsTableBody">
|
|
<tr>
|
|
<td colspan="8" class="no-logs">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
<p>Caricamento log...</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="pagination" id="pagination" style="display: none;">
|
|
<button id="prevBtn" onclick="changePage(-1)">← Precedente</button>
|
|
<span class="pagination-info" id="pageInfo">Pagina 1</span>
|
|
<button id="nextBtn" onclick="changePage(1)">Successiva →</button>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
let allLogs = [];
|
|
let filteredLogs = [];
|
|
let currentPage = 1;
|
|
const logsPerPage = 50;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadLogs();
|
|
loadUsers();
|
|
});
|
|
|
|
async function loadUsers() {
|
|
const result = await apiCall('/api/admin/users');
|
|
if (result.status === 'success') {
|
|
const filterUser = document.getElementById('filterUser');
|
|
result.data.forEach(user => {
|
|
const option = document.createElement('option');
|
|
option.value = user.username;
|
|
option.textContent = user.username;
|
|
filterUser.appendChild(option);
|
|
});
|
|
}
|
|
}
|
|
|
|
async function loadLogs() {
|
|
const limit = document.getElementById('filterLimit').value;
|
|
const result = await apiCall(`/api/admin/logs?limit=${limit}`);
|
|
|
|
if (result.status === 'success') {
|
|
allLogs = result.data;
|
|
filterLogs();
|
|
} else {
|
|
document.getElementById('logsTableBody').innerHTML = `
|
|
<tr><td colspan="8" class="no-logs">
|
|
<div class="alert alert-error">${result.message}</div>
|
|
</td></tr>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function filterLogs() {
|
|
const filterUser = document.getElementById('filterUser').value.toLowerCase();
|
|
const filterAction = document.getElementById('filterAction').value.toLowerCase();
|
|
const filterStatus = document.getElementById('filterStatus').value.toLowerCase();
|
|
const filterVmId = document.getElementById('filterVmId').value;
|
|
const filterDate = document.getElementById('filterDate').value;
|
|
|
|
filteredLogs = allLogs.filter(log => {
|
|
const matchUser = !filterUser || log.username.toLowerCase().includes(filterUser);
|
|
const matchAction = !filterAction || log.action_type === filterAction;
|
|
const matchStatus = !filterStatus || log.status === filterStatus;
|
|
const matchVmId = !filterVmId || log.vm_id.toString() === filterVmId;
|
|
const matchDate = !filterDate || new Date(log.created_at).toISOString().split('T')[0] === filterDate;
|
|
|
|
return matchUser && matchAction && matchStatus && matchVmId && matchDate;
|
|
});
|
|
|
|
currentPage = 1;
|
|
renderLogs();
|
|
}
|
|
|
|
function renderLogs() {
|
|
const tbody = document.getElementById('logsTableBody');
|
|
const pagination = document.getElementById('pagination');
|
|
|
|
if (filteredLogs.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="8" class="no-logs">Nessun log trovato con i filtri applicati</td></tr>';
|
|
pagination.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const totalPages = Math.ceil(filteredLogs.length / logsPerPage);
|
|
const startIdx = (currentPage - 1) * logsPerPage;
|
|
const endIdx = startIdx + logsPerPage;
|
|
const logsToShow = filteredLogs.slice(startIdx, endIdx);
|
|
|
|
const actionIcons = {
|
|
'start': '▶️',
|
|
'stop': '⏹️',
|
|
'restart': '🔄',
|
|
'shutdown': '🛑',
|
|
'snapshot': '📸',
|
|
'backup': '💾',
|
|
'login': '🔐'
|
|
};
|
|
|
|
tbody.innerHTML = logsToShow.map(log => {
|
|
const date = new Date(log.created_at);
|
|
const formattedDate = date.toLocaleString('it-IT');
|
|
const icon = actionIcons[log.action_type] || '•';
|
|
|
|
return `
|
|
<tr>
|
|
<td><strong>#${log.id}</strong></td>
|
|
<td>${formattedDate}</td>
|
|
<td><strong>${log.username}</strong></td>
|
|
<td><span class="log-action-badge">${icon} ${log.action_type}</span></td>
|
|
<td>${log.vm_id > 0 ? `VM ${log.vm_id}` : '-'}</td>
|
|
<td><span class="log-status ${log.status}">${log.status === 'success' ? 'Successo' : 'Fallito'}</span></td>
|
|
<td><code>${log.ip_address || '-'}</code></td>
|
|
<td>
|
|
${log.error_message ? `<div class="log-error-message">${log.error_message}</div>` : '-'}
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
// Update pagination
|
|
document.getElementById('pageInfo').textContent = `Pagina ${currentPage} di ${totalPages} (${filteredLogs.length} risultati)`;
|
|
document.getElementById('prevBtn').disabled = currentPage === 1;
|
|
document.getElementById('nextBtn').disabled = currentPage === totalPages;
|
|
pagination.style.display = 'flex';
|
|
}
|
|
|
|
function changePage(delta) {
|
|
currentPage += delta;
|
|
renderLogs();
|
|
}
|
|
|
|
function exportLogs() {
|
|
if (filteredLogs.length === 0) {
|
|
showAlert('Nessun log da esportare', 'warning');
|
|
return;
|
|
}
|
|
|
|
const headers = ['ID', 'Data/Ora', 'Utente', 'Azione', 'VM ID', 'Stato', 'IP', 'Errore'];
|
|
const rows = filteredLogs.map(log => [
|
|
log.id,
|
|
new Date(log.created_at).toLocaleString('it-IT'),
|
|
log.username,
|
|
log.action_type,
|
|
log.vm_id,
|
|
log.status,
|
|
log.ip_address || '',
|
|
log.error_message || ''
|
|
]);
|
|
|
|
let csvContent = headers.join(',') + '\n';
|
|
csvContent += rows.map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
|
|
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
const url = URL.createObjectURL(blob);
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', `system_logs_${new Date().toISOString().split('T')[0]}.csv`);
|
|
link.style.visibility = 'hidden';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
showAlert('Log esportati con successo', 'success');
|
|
}
|
|
</script>
|
|
{% endblock %}
|