First Commit
This commit is contained in:
367
templates/base.html
Normal file
367
templates/base.html
Normal file
@@ -0,0 +1,367 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Proxmox Manager{% endblock %}</title>
|
||||
|
||||
<!-- Chart.js per grafici -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
|
||||
<!-- SweetAlert2 per dialoghi -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/theme.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}">
|
||||
|
||||
<!-- SweetAlert2 Dark Theme Override -->
|
||||
<style>
|
||||
/* SweetAlert2 Dark Theme */
|
||||
.swal2-popup {
|
||||
background: var(--bg-secondary) !important;
|
||||
border: 1px solid var(--border-default) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.swal2-title {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.swal2-html-container {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
.swal2-confirm {
|
||||
background: var(--accent-blue) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
}
|
||||
|
||||
.swal2-cancel {
|
||||
background: var(--bg-tertiary) !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
}
|
||||
|
||||
.swal2-timer-progress-bar {
|
||||
background: var(--accent-blue) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block extra_styles %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% if current_user.is_authenticated %}
|
||||
<nav class="navbar">
|
||||
<a href="{{ url_for('dashboard') }}" class="navbar-brand">
|
||||
<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>
|
||||
Proxmox Manager
|
||||
</a>
|
||||
<div class="navbar-menu">
|
||||
<a href="{{ url_for('dashboard') }}" class="{{ 'active' if request.endpoint == 'dashboard' else '' }}">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="{{ url_for('overview') }}" class="{{ 'active' if request.endpoint == 'overview' else '' }}">
|
||||
Overview
|
||||
</a>
|
||||
<a href="{{ url_for('domains') }}" class="{{ 'active' if request.endpoint == 'domains' else '' }}">
|
||||
Domini
|
||||
</a>
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('admin_users') }}" class="{{ 'active' if request.endpoint == 'admin_users' else '' }}">
|
||||
Gestione Utenti
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('profile') }}" class="{{ 'active' if request.endpoint == 'profile' else '' }}">
|
||||
Profilo
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-user">
|
||||
<span>{{ current_user.username }}</span>
|
||||
{% if current_user.is_admin %}
|
||||
<span class="badge-admin">ADMIN</span>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('logout') }}" class="btn btn-ghost btn-sm">Logout</a>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
<!-- Notification Container -->
|
||||
<div id="notification-container" class="notification-container"></div>
|
||||
|
||||
<script>
|
||||
// Helper function per chiamate API
|
||||
async function apiCall(url, method = 'GET', data = null) {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
if (data) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const result = await response.json();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('API Error:', error);
|
||||
return { status: 'error', message: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Helper per mostrare alert con SweetAlert2
|
||||
function showAlert(message, type = 'info') {
|
||||
const icons = {
|
||||
'success': 'success',
|
||||
'error': 'error',
|
||||
'info': 'info',
|
||||
'warning': 'warning'
|
||||
};
|
||||
|
||||
Swal.fire({
|
||||
icon: icons[type] || 'info',
|
||||
title: type === 'error' ? 'Errore' : type === 'success' ? 'Successo' : 'Info',
|
||||
text: message,
|
||||
showConfirmButton: true,
|
||||
timer: type === 'success' ? 3000 : undefined,
|
||||
timerProgressBar: type === 'success',
|
||||
background: 'var(--bg-secondary)',
|
||||
color: 'var(--text-primary)',
|
||||
confirmButtonColor: 'var(--accent-blue)'
|
||||
});
|
||||
}
|
||||
|
||||
// Toast notification leggera
|
||||
function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('notification-container');
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `notification-toast notification-${type}`;
|
||||
|
||||
const icons = {
|
||||
'success': '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>',
|
||||
'error': '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>',
|
||||
'info': '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>',
|
||||
'warning': '<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>'
|
||||
};
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="notification-icon">${icons[type] || icons.info}</div>
|
||||
<div class="notification-message">${message}</div>
|
||||
<button class="notification-close" onclick="this.parentElement.remove()">×</button>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Trigger animation
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add('show');
|
||||
});
|
||||
|
||||
// Auto dismiss
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Helper per formattare timestamp
|
||||
function formatDate(timestamp) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString('it-IT');
|
||||
}
|
||||
|
||||
// Helper per formattare byte
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes || bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// Helper per formattare uptime
|
||||
function formatUptime(seconds) {
|
||||
if (!seconds) return 'N/A';
|
||||
const days = Math.floor(seconds / 86400);
|
||||
const hours = Math.floor((seconds % 86400) / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (days > 0) return `${days}g ${hours}h ${minutes}m`;
|
||||
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
// Notification System with SSE
|
||||
class NotificationSystem {
|
||||
constructor() {
|
||||
this.eventSource = null;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
}
|
||||
|
||||
this.eventSource = new EventSource('/api/notifications/stream');
|
||||
|
||||
this.eventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type !== 'ping') {
|
||||
showToast(data.message, data.notification_type || 'info');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing notification:', e);
|
||||
}
|
||||
};
|
||||
|
||||
this.eventSource.onerror = () => {
|
||||
this.eventSource.close();
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
setTimeout(() => this.connect(), 5000);
|
||||
}
|
||||
};
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
this.reconnectAttempts = 0;
|
||||
};
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize notification system
|
||||
const notificationSystem = new NotificationSystem();
|
||||
|
||||
// Connect if user is authenticated
|
||||
{% if current_user.is_authenticated %}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
notificationSystem.connect();
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
|
||||
<!-- Notification Styles -->
|
||||
<style>
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 80px;
|
||||
right: var(--space-lg);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
max-width: 400px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.notification-toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
padding: var(--space-md);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateX(120%);
|
||||
transition: transform 0.3s ease;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.notification-toast.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.notification-toast.notification-success {
|
||||
border-left: 4px solid var(--accent-green);
|
||||
}
|
||||
|
||||
.notification-toast.notification-success .notification-icon {
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.notification-toast.notification-error {
|
||||
border-left: 4px solid var(--accent-red);
|
||||
}
|
||||
|
||||
.notification-toast.notification-error .notification-icon {
|
||||
color: var(--accent-red);
|
||||
}
|
||||
|
||||
.notification-toast.notification-warning {
|
||||
border-left: 4px solid var(--accent-yellow);
|
||||
}
|
||||
|
||||
.notification-toast.notification-warning .notification-icon {
|
||||
color: var(--accent-yellow);
|
||||
}
|
||||
|
||||
.notification-toast.notification-info {
|
||||
border-left: 4px solid var(--accent-blue);
|
||||
}
|
||||
|
||||
.notification-toast.notification-info .notification-icon {
|
||||
color: var(--accent-blue);
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
flex: 1;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.notification-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
font-size: 1.25rem;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.notification-close:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user