Files
proxmox_manager/templates/base.html
2026-02-17 12:43:27 +01:00

368 lines
13 KiB
HTML

<!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()">&times;</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>