First Commit

This commit is contained in:
dedhersel
2026-02-17 12:43:27 +01:00
commit 38f85dc498
26 changed files with 9939 additions and 0 deletions

View File

@@ -0,0 +1,361 @@
{% extends "base.html" %}
{% block title %}Admin Dashboard - Proxmox Manager{% endblock %}
{% block extra_styles %}
<style>
.admin-stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: var(--space-md);
margin-bottom: var(--space-xl);
}
.admin-stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: var(--space-lg);
display: flex;
align-items: center;
gap: var(--space-md);
transition: all var(--transition-fast);
}
.admin-stat-card:hover {
border-color: var(--border-muted);
transform: translateY(-2px);
}
.admin-stat-card .stat-icon {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.admin-stat-card.users .stat-icon {
background: rgba(88, 166, 255, 0.15);
color: var(--accent-blue);
}
.admin-stat-card.vms .stat-icon {
background: rgba(163, 113, 247, 0.15);
color: var(--accent-purple);
}
.admin-stat-card.active .stat-icon {
background: rgba(63, 185, 80, 0.15);
color: var(--accent-green);
}
.admin-stat-card.actions .stat-icon {
background: rgba(210, 153, 34, 0.15);
color: var(--accent-yellow);
}
.admin-stat-card .stat-label {
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: var(--space-xs);
}
.admin-stat-card .stat-number {
font-size: 1.75rem;
font-weight: 700;
color: var(--text-primary);
}
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--space-md);
margin-bottom: var(--space-xl);
}
.action-card {
background: var(--bg-secondary);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: var(--space-lg);
text-align: center;
cursor: pointer;
transition: all var(--transition-fast);
text-decoration: none;
}
.action-card:hover {
transform: translateY(-3px);
border-color: var(--accent-blue);
box-shadow: var(--shadow-md);
}
.action-icon {
width: 48px;
height: 48px;
margin: 0 auto var(--space-md);
background: var(--bg-tertiary);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-blue);
}
.action-title {
font-weight: 600;
color: var(--text-primary);
font-size: 0.9rem;
}
.recent-activity {
max-height: 450px;
overflow-y: auto;
}
.activity-item {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: var(--space-md);
background: var(--bg-tertiary);
border-radius: var(--radius-md);
margin-bottom: var(--space-sm);
border-left: 3px solid var(--accent-blue);
}
.activity-item.success {
border-left-color: var(--accent-green);
}
.activity-item.failed {
border-left-color: var(--accent-red);
}
.activity-user {
font-weight: 600;
color: var(--text-primary);
}
.activity-action {
color: var(--accent-blue);
font-weight: 500;
}
.activity-time {
font-size: 0.8rem;
color: var(--text-tertiary);
white-space: nowrap;
}
.activity-error {
font-size: 0.8rem;
color: var(--accent-red);
margin-top: var(--space-xs);
}
</style>
{% endblock %}
{% block content %}
<div class="dashboard-header">
<div>
<h1>Dashboard Amministratore</h1>
<p class="subtitle">Panoramica del sistema</p>
</div>
</div>
<div class="admin-stats-grid" id="statsGrid">
<div class="admin-stat-card users">
<div class="stat-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</div>
<div>
<div class="stat-label">Utenti Totali</div>
<div class="stat-number" id="totalUsers">-</div>
</div>
</div>
<div class="admin-stat-card vms">
<div class="stat-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
</div>
<div>
<div class="stat-label">VM Gestite</div>
<div class="stat-number" id="totalVMs">-</div>
</div>
</div>
<div class="admin-stat-card active">
<div class="stat-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
</svg>
</div>
<div>
<div class="stat-label">VM Attive</div>
<div class="stat-number" id="activeVMs">-</div>
</div>
</div>
<div class="admin-stat-card actions">
<div class="stat-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
</svg>
</div>
<div>
<div class="stat-label">Azioni Oggi</div>
<div class="stat-number" id="actionsToday">-</div>
</div>
</div>
</div>
<div class="quick-actions">
<a class="action-card" href="{{ url_for('admin_users') }}">
<div class="action-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</div>
<div class="action-title">Gestione Utenti</div>
</a>
<a class="action-card" href="{{ url_for('system_logs') }}">
<div class="action-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
</div>
<div class="action-title">Log di Sistema</div>
</a>
<a class="action-card" href="{{ url_for('overview') }}">
<div class="action-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
</div>
<div class="action-title">Overview VM</div>
</a>
<a class="action-card" href="{{ url_for('cluster_topology') }}">
<div class="action-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<circle cx="6" cy="6" r="2"/>
<circle cx="18" cy="6" r="2"/>
<circle cx="6" cy="18" r="2"/>
<circle cx="18" cy="18" r="2"/>
<line x1="10.5" y1="10.5" x2="7.5" y2="7.5"/>
<line x1="13.5" y1="10.5" x2="16.5" y2="7.5"/>
<line x1="10.5" y1="13.5" x2="7.5" y2="16.5"/>
<line x1="13.5" y1="13.5" x2="16.5" y2="16.5"/>
</svg>
</div>
<div class="action-title">Topologia Cluster</div>
</a>
<a class="action-card" href="{{ url_for('ipam') }}">
<div class="action-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
<line x1="6" y1="6" x2="6.01" y2="6"/>
<line x1="6" y1="18" x2="6.01" y2="18"/>
</svg>
</div>
<div class="action-title">Gestione IPAM</div>
</a>
</div>
<div class="card">
<div class="card-title">Attività Recente</div>
<div class="recent-activity" id="recentActivity">
<div class="loading">
<div class="spinner"></div>
<p>Caricamento attività...</p>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
document.addEventListener('DOMContentLoaded', function() {
loadDashboardStats();
loadRecentActivity();
// Ricarica ogni 30 secondi
setInterval(loadRecentActivity, 30000);
});
async function loadDashboardStats() {
const statsResult = await apiCall('/api/admin/stats');
if (statsResult.status === 'success') {
const stats = statsResult.data;
document.getElementById('totalUsers').textContent = stats.total_users;
document.getElementById('totalVMs').textContent = stats.total_vms;
document.getElementById('activeVMs').textContent = stats.active_vms;
document.getElementById('actionsToday').textContent = stats.actions_today;
} else {
console.error('Errore caricamento statistiche:', statsResult);
}
}
async function loadRecentActivity() {
const container = document.getElementById('recentActivity');
const result = await apiCall('/api/admin/logs?limit=20');
if (result.status === 'success') {
if (result.data.length === 0) {
container.innerHTML = '<p class="text-muted text-center" style="padding: var(--space-xl);">Nessuna attività registrata</p>';
return;
}
container.innerHTML = result.data.map(log => {
const statusClass = log.status === 'success' ? 'success' : 'failed';
const actionIcons = {
'start': '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M10.804 8 5 4.633v6.734L10.804 8z"/></svg>',
'stop': '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><rect width="8" height="8" x="4" y="4"/></svg>',
'restart': '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/><path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/></svg>',
'shutdown': '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M7.5 1v7h1V1h-1z"/><path d="M3 8.812a4.999 4.999 0 0 1 2.578-4.375l-.485-.874A6 6 0 1 0 11 3.616l-.501.865A5 5 0 1 1 3 8.812z"/></svg>',
'backup': '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><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 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg>',
'login': '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M6 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 0-1 0v2A1.5 1.5 0 0 0 6.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-8A1.5 1.5 0 0 0 5 3.5v2a.5.5 0 0 0 1 0v-2z"/><path fill-rule="evenodd" d="M11.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H1.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/></svg>'
};
const icon = actionIcons[log.action_type] || '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><circle cx="8" cy="8" r="3"/></svg>';
return `
<div class="activity-item ${statusClass}">
<div>
<span class="activity-user">${log.username}</span>
<span style="color: var(--text-secondary); margin: 0 var(--space-xs);">${icon}</span>
<span class="activity-action">${log.action_type}</span>
${log.vm_id > 0 ? `<span class="text-muted">su VM ${log.vm_id}</span>` : ''}
${log.status === 'failed' && log.error_message ? `<div class="activity-error">${log.error_message}</div>` : ''}
</div>
<div class="activity-time">${new Date(log.created_at).toLocaleString('it-IT')}</div>
</div>
`;
}).join('');
} else {
container.innerHTML = `<div class="alert alert-error">${result.message}</div>`;
}
}
</script>
{% endblock %}