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

860 lines
33 KiB
HTML

{% extends "base.html" %}
{% block title %}Dashboard - Proxmox Manager{% endblock %}
{% block content %}
<div class="dashboard-header">
<div>
<h1>Le Mie Macchine Virtuali</h1>
<p class="subtitle">Gestisci le tue VM direttamente da qui</p>
</div>
<div class="dashboard-actions">
<button class="btn btn-ghost btn-sm" onclick="loadVMs()">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<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>
Aggiorna
</button>
</div>
</div>
<div id="vms-container">
<div class="loading">
<div class="spinner"></div>
<p>Caricamento VM in corso...</p>
</div>
</div>
<!-- Modal per i backup -->
<div id="backupModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeBackupModal()">&times;</span>
<h2 id="backupModalTitle">Backup di VM</h2>
<div id="backupModalContent">
<div class="loading">
<div class="spinner"></div>
<p>Caricamento backup...</p>
</div>
</div>
</div>
</div>
<!-- Modal per le snapshot -->
<div id="snapshotModal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeSnapshotModal()">&times;</span>
<h2 id="snapshotModalTitle">Snapshot di VM</h2>
<div id="snapshotModalContent">
<div class="loading">
<div class="spinner"></div>
<p>Caricamento snapshot...</p>
</div>
</div>
</div>
</div>
<!-- Modal per la console -->
<div id="consoleModal" class="modal">
<div class="modal-content" style="max-width: 95vw; width: 1200px; height: 85vh; padding: 0; overflow: hidden;">
<div class="console-header">
<span id="consoleModalTitle">Console VM</span>
<div class="console-toolbar">
<button class="btn btn-ghost btn-sm" onclick="sendCtrlAltDel()">Ctrl+Alt+Del</button>
<button class="btn btn-ghost btn-sm" onclick="toggleConsoleFullscreen()">Fullscreen</button>
<button class="btn btn-ghost btn-sm" onclick="closeConsoleModal()">&times;</button>
</div>
</div>
<div id="consoleContainer" style="width: 100%; height: calc(100% - 48px); background: #000;"></div>
</div>
</div>
<!-- Modal per grafici storici -->
<div id="historyModal" class="modal">
<div class="modal-content" style="max-width: 900px; width: 90%;">
<span class="close" onclick="closeHistoryModal()">&times;</span>
<h2 id="historyModalTitle">Storico Metriche</h2>
<div class="chart-controls" style="margin-bottom: var(--space-md);">
<button class="chart-btn active" data-timeframe="hour" onclick="loadHistoricalData('hour')">1H</button>
<button class="chart-btn" data-timeframe="day" onclick="loadHistoricalData('day')">24H</button>
<button class="chart-btn" data-timeframe="week" onclick="loadHistoricalData('week')">7G</button>
<button class="chart-btn" data-timeframe="month" onclick="loadHistoricalData('month')">30G</button>
</div>
<div id="historyChartContainer" style="height: 400px; position: relative;">
<canvas id="historyChart"></canvas>
</div>
</div>
</div>
{% endblock %}
{% block extra_styles %}
<style>
.console-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-sm) var(--space-md);
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-default);
}
.console-header span {
font-weight: 600;
color: var(--text-primary);
}
.console-toolbar {
display: flex;
gap: var(--space-sm);
}
#consoleContainer iframe {
width: 100%;
height: 100%;
border: none;
}
</style>
{% endblock %}
{% block extra_scripts %}
<script>
let currentVMs = [];
let cpuCharts = {};
let historyChart = null;
let currentHistoryVmId = null;
// Carica le VM al caricamento della pagina
document.addEventListener('DOMContentLoaded', function() {
loadVMs();
// Ricarica metriche ogni 5 secondi
setInterval(refreshMetrics, 5000);
});
function initCPUChart(vmId, cpuPercent) {
const canvas = document.getElementById(`cpu-chart-${vmId}`);
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (cpuCharts[vmId]) {
cpuCharts[vmId].destroy();
}
cpuCharts[vmId] = new Chart(ctx, {
type: 'line',
data: {
labels: Array(10).fill(''),
datasets: [{
data: Array(10).fill(cpuPercent),
borderColor: 'rgba(88, 166, 255, 0.8)',
backgroundColor: 'rgba(88, 166, 255, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: { enabled: false }
},
scales: {
x: { display: false },
y: { display: false, min: 0, max: 100 }
},
animation: { duration: 500 }
}
});
}
function updateCPUChart(vmId, newValue) {
const chart = cpuCharts[vmId];
if (!chart) return;
chart.data.datasets[0].data.shift();
chart.data.datasets[0].data.push(newValue);
chart.update('none');
}
async function refreshMetrics() {
if (currentVMs.length === 0) return;
for (const vm of currentVMs) {
try {
const result = await apiCall(`/api/vm/${vm.vm_id}/status`);
if (result.status === 'success') {
const vmData = result.data;
const vmCard = document.querySelector(`[data-vm-id="${vm.vm_id}"]`);
if (!vmCard) continue;
const cpuPercent = vmData.cpu ? Math.round(vmData.cpu * 100) : 0;
const memPercent = vmData.maxmem ? Math.round((vmData.mem / vmData.maxmem) * 100) : 0;
const diskPercent = vmData.maxdisk ? Math.round((vmData.disk / vmData.maxdisk) * 100) : 0;
// Aggiorna valori CPU
const cpuValue = vmCard.querySelector('.metric-box.cpu .metric-value');
if (cpuValue) {
cpuValue.textContent = cpuPercent + '%';
updateCPUChart(vm.vm_id, cpuPercent);
}
// Aggiorna valori RAM
const memValue = vmCard.querySelector('.metric-box.memory .metric-value');
const memSubvalue = vmCard.querySelector('.metric-box.memory .metric-subvalue');
if (memValue) memValue.textContent = memPercent + '%';
if (memSubvalue) memSubvalue.textContent = `${formatBytes(vmData.mem)} / ${formatBytes(vmData.maxmem)}`;
// Aggiorna valori Disk
const diskValue = vmCard.querySelector('.metric-box.disk .metric-value');
const diskSubvalue = vmCard.querySelector('.metric-box.disk .metric-subvalue');
if (diskValue) diskValue.textContent = diskPercent + '%';
if (diskSubvalue) diskSubvalue.textContent = `${formatBytes(vmData.disk || 0)} / ${formatBytes(vmData.maxdisk || 0)}`;
// Aggiorna uptime
const uptimeValue = vmCard.querySelector('.stat-uptime');
if (uptimeValue) uptimeValue.textContent = formatUptime(vmData.uptime);
// Se lo stato cambia, ricarica
if (vm.status !== vmData.status) {
loadVMs();
break;
}
}
} catch (error) {
console.error(`Errore refresh metriche VM ${vm.vm_id}:`, error);
}
}
}
async function loadVMs() {
const container = document.getElementById('vms-container');
const result = await apiCall('/api/my-vms');
if (result.status === 'success') {
currentVMs = result.data;
if (currentVMs.length === 0) {
container.innerHTML = `
<div class="no-vms">
<div class="no-vms-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<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>
<h2>Nessuna VM assegnata</h2>
<p>Contatta l'amministratore per richiedere l'accesso a una VM</p>
</div>
`;
return;
}
container.innerHTML = '<div class="vm-grid">' +
currentVMs.map(vm => createVMCard(vm)).join('') +
'</div>';
// Inizializza grafici CPU
setTimeout(() => {
currentVMs.forEach(vm => {
const cpuPercent = vm.cpu ? Math.round(vm.cpu * 100) : 0;
initCPUChart(vm.vm_id, cpuPercent);
});
}, 100);
} else {
container.innerHTML = `
<div class="alert alert-error">
Errore nel caricamento delle VM: ${result.message}
</div>
`;
}
}
function createVMCard(vm) {
const statusClass = vm.status === 'running' ? 'status-running' :
vm.status === 'stopped' ? 'status-stopped' : 'status-unknown';
const statusText = vm.status === 'running' ? 'Running' :
vm.status === 'stopped' ? 'Stopped' : 'Unknown';
const memPercent = vm.maxmem ? Math.round((vm.mem / vm.maxmem) * 100) : 0;
const cpuPercent = vm.cpu ? Math.round(vm.cpu * 100) : 0;
const diskPercent = vm.maxdisk ? Math.round((vm.disk / vm.maxdisk) * 100) : 0;
return `
<div class="vm-card ${statusClass}" data-vm-id="${vm.vm_id}">
<div class="vm-header">
<div>
<div class="vm-title">${vm.vm_name}</div>
<div class="vm-id">VM ID: ${vm.vm_id} | Node: ${vm.node}</div>
</div>
<span class="status-badge ${statusClass}">${statusText}</span>
</div>
${vm.notes ? `<div class="vm-notes">${vm.notes}</div>` : ''}
<div class="metrics-realtime">
<div class="metric-box cpu">
<div class="metric-label">CPU</div>
<div class="metric-value">${cpuPercent}%</div>
<canvas id="cpu-chart-${vm.vm_id}"></canvas>
</div>
<div class="metric-box memory">
<div class="metric-label">RAM</div>
<div class="metric-value">${memPercent}%</div>
<div class="metric-subvalue">${formatBytes(vm.mem)} / ${formatBytes(vm.maxmem)}</div>
</div>
<div class="metric-box disk">
<div class="metric-label">Disco</div>
<div class="metric-value">${diskPercent}%</div>
<div class="metric-subvalue">${formatBytes(vm.disk || 0)} / ${formatBytes(vm.maxdisk || 0)}</div>
</div>
</div>
<div class="vm-stats">
<div class="stat-item">
<div class="stat-label">Uptime</div>
<div class="stat-value stat-uptime">${formatUptime(vm.uptime)}</div>
</div>
<div class="stat-item">
<div class="stat-label">Tipo</div>
<div class="stat-value">${vm.type || 'QEMU'}</div>
</div>
${vm.ip_address ? `
<div class="stat-item">
<div class="stat-label">Indirizzo IP</div>
<div class="stat-value"><code style="font-size: 0.85rem;">${vm.ip_address}</code></div>
</div>
` : ''}
</div>
<div class="vm-actions">
${vm.status === 'running' ? `
<button class="btn btn-warning" onclick="shutdownVM(${vm.vm_id})">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><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>
Spegni
</button>
<button class="btn btn-secondary" onclick="rebootVM(${vm.vm_id})">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><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>
Riavvia
</button>
<button class="btn btn-primary" onclick="openConsole(${vm.vm_id}, '${vm.vm_name}')">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect width="14" height="10" x="1" y="2" rx="1"/><path d="M1 14h14v1H1z"/></svg>
Console
</button>
` : `
<button class="btn btn-success" onclick="startVM(${vm.vm_id})">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M10.804 8 5 4.633v6.734L10.804 8zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696l6.363 3.692z"/></svg>
Avvia
</button>
`}
<button class="btn btn-secondary" onclick="showHistory(${vm.vm_id}, '${vm.vm_name}')">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5h-2v12h2V2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z"/></svg>
Storico
</button>
<button class="btn btn-secondary" onclick="showBackups(${vm.vm_id}, '${vm.vm_name}')">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><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>
Backup
</button>
<button class="btn btn-secondary" onclick="showSnapshots(${vm.vm_id}, '${vm.vm_name}')">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M10.5 8.5a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/><path d="M2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4H2zm.5 2a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 2.5a3.5 3.5 0 1 1-7 0 3.5 3.5 0 0 1 7 0z"/></svg>
Snapshot
</button>
</div>
</div>
`;
}
// ==================== VM ACTIONS ====================
async function startVM(vmId) {
const result = await Swal.fire({
title: 'Avvia VM',
text: 'Vuoi avviare questa macchina virtuale?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Avvia',
cancelButtonText: 'Annulla'
});
if (!result.isConfirmed) return;
showToast('Avvio VM in corso...', 'info');
const response = await apiCall(`/api/vm/${vmId}/start`, 'POST');
if (response.status === 'success') {
showToast('VM avviata con successo!', 'success');
setTimeout(loadVMs, 2000);
} else {
showAlert('Errore: ' + response.message, 'error');
}
}
async function shutdownVM(vmId) {
const result = await Swal.fire({
title: 'Spegni VM',
text: 'Vuoi spegnere questa macchina virtuale? (shutdown graceful)',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Spegni',
cancelButtonText: 'Annulla'
});
if (!result.isConfirmed) return;
showToast('Spegnimento VM in corso...', 'info');
const response = await apiCall(`/api/vm/${vmId}/shutdown`, 'POST');
if (response.status === 'success') {
showToast('VM in fase di spegnimento...', 'success');
setTimeout(loadVMs, 2000);
} else {
showAlert('Errore: ' + response.message, 'error');
}
}
async function rebootVM(vmId) {
const result = await Swal.fire({
title: 'Riavvia VM',
text: 'Vuoi riavviare questa macchina virtuale?',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Riavvia',
cancelButtonText: 'Annulla'
});
if (!result.isConfirmed) return;
showToast('Riavvio VM in corso...', 'info');
const response = await apiCall(`/api/vm/${vmId}/reboot`, 'POST');
if (response.status === 'success') {
showToast('VM in fase di riavvio...', 'success');
setTimeout(loadVMs, 2000);
} else {
showAlert('Errore: ' + response.message, 'error');
}
}
// ==================== CONSOLE ====================
async function openConsole(vmId, vmName) {
showToast('Apertura console...', 'info');
const result = await apiCall(`/api/vm/${vmId}/console`);
if (result.status === 'success') {
document.getElementById('consoleModalTitle').textContent = `Console: ${vmName} (VM ${vmId})`;
const container = document.getElementById('consoleContainer');
container.innerHTML = `
<iframe
src="${result.console_url}"
allow="clipboard-read; clipboard-write"
style="width: 100%; height: 100%; border: none;"
></iframe>
`;
document.getElementById('consoleModal').style.display = 'block';
showToast('Console aperta. Potrebbe essere necessario autenticarsi su Proxmox.', 'info');
} else {
showAlert('Errore: ' + result.message, 'error');
}
}
function closeConsoleModal() {
document.getElementById('consoleModal').style.display = 'none';
document.getElementById('consoleContainer').innerHTML = '';
}
function toggleConsoleFullscreen() {
const modal = document.getElementById('consoleModal').querySelector('.modal-content');
modal.requestFullscreen?.() || modal.webkitRequestFullscreen?.() || modal.msRequestFullscreen?.();
}
function sendCtrlAltDel() {
showToast('Funzione disponibile solo con noVNC integrato', 'warning');
}
// ==================== HISTORY CHARTS ====================
function showHistory(vmId, vmName) {
currentHistoryVmId = vmId;
document.getElementById('historyModalTitle').textContent = `Storico Metriche: ${vmName}`;
document.getElementById('historyModal').style.display = 'block';
// Reset active button
document.querySelectorAll('.chart-btn').forEach(btn => btn.classList.remove('active'));
document.querySelector('.chart-btn[data-timeframe="hour"]').classList.add('active');
loadHistoricalData('hour');
}
async function loadHistoricalData(timeframe) {
if (!currentHistoryVmId) return;
// Update active button
document.querySelectorAll('.chart-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.timeframe === timeframe);
});
const result = await apiCall(`/api/vm/${currentHistoryVmId}/rrddata?timeframe=${timeframe}`);
if (result.status === 'success') {
renderHistoryChart(result.data, timeframe);
} else {
showAlert('Errore caricamento dati storici: ' + result.message, 'error');
}
}
function renderHistoryChart(data, timeframe) {
const ctx = document.getElementById('historyChart').getContext('2d');
if (historyChart) {
historyChart.destroy();
}
const labels = data.map(d => {
const date = new Date(d.time * 1000);
if (timeframe === 'hour') {
return date.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
} else if (timeframe === 'day') {
return date.toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' });
} else {
return date.toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit' });
}
});
historyChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'CPU %',
data: data.map(d => (d.cpu || 0) * 100),
borderColor: '#58a6ff',
backgroundColor: 'rgba(88, 166, 255, 0.1)',
fill: true,
tension: 0.3,
pointRadius: 0
},
{
label: 'RAM %',
data: data.map(d => {
const mem = d.mem || d.memory || 0;
const maxmem = d.maxmem || 1;
return (mem / maxmem) * 100;
}),
borderColor: '#a371f7',
backgroundColor: 'rgba(163, 113, 247, 0.1)',
fill: true,
tension: 0.3,
pointRadius: 0
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
legend: {
display: true,
labels: { color: '#8b949e' }
},
tooltip: {
backgroundColor: '#21262d',
titleColor: '#e6edf3',
bodyColor: '#8b949e',
borderColor: '#30363d',
borderWidth: 1
}
},
scales: {
x: {
display: true,
grid: { color: '#21262d' },
ticks: { color: '#6e7681', maxTicksLimit: 10 }
},
y: {
display: true,
min: 0,
max: 100,
grid: { color: '#21262d' },
ticks: {
color: '#6e7681',
callback: value => value + '%'
}
}
}
}
});
}
function closeHistoryModal() {
document.getElementById('historyModal').style.display = 'none';
currentHistoryVmId = null;
}
// ==================== BACKUPS ====================
async function showBackups(vmId, vmName) {
const modal = document.getElementById('backupModal');
const title = document.getElementById('backupModalTitle');
const content = document.getElementById('backupModalContent');
title.textContent = `Backup: ${vmName} (VM ${vmId})`;
content.innerHTML = '<div class="loading"><div class="spinner"></div><p>Caricamento backup...</p></div>';
modal.style.display = 'block';
const result = await apiCall(`/api/vm/${vmId}/backups`);
if (result.status === 'success') {
if (result.data.length === 0) {
content.innerHTML = `
<p class="text-muted text-center" style="padding: var(--space-xl);">Nessun backup disponibile</p>
<div style="text-align: center;">
<button class="btn btn-primary" onclick="createBackup(${vmId})">Crea Backup</button>
</div>
`;
} else {
content.innerHTML = `
<p class="text-muted mb-2">Trovati ${result.count} backup</p>
<div class="backup-list">
${result.data.map(backup => {
const volid = backup.volid || '';
const parts = volid.split('/');
const filename = parts[parts.length - 1];
return `
<div class="backup-item">
<div class="backup-info">
<div class="backup-name">${filename}</div>
<div class="backup-date">${formatDate(backup.ctime || 0)}</div>
</div>
<div style="display: flex; align-items: center; gap: var(--space-md);">
<span class="backup-size">${formatBytes(backup.size || 0)}</span>
<button class="btn btn-danger btn-sm"
onclick="deleteBackup('${backup.node}', '${backup.storage}', '${filename}', ${vmId})">
Elimina
</button>
</div>
</div>
`;
}).join('')}
</div>
<div style="text-align: center; margin-top: var(--space-lg);">
<button class="btn btn-primary" onclick="createBackup(${vmId})">Crea Nuovo Backup</button>
</div>
`;
}
} else {
content.innerHTML = `<div class="alert alert-error">${result.message}</div>`;
}
}
async function createBackup(vmId) {
const result = await Swal.fire({
title: 'Crea Backup',
text: 'Vuoi creare un backup di questa VM? L\'operazione potrebbe richiedere diversi minuti.',
icon: 'question',
showCancelButton: true,
confirmButtonText: 'Crea Backup',
cancelButtonText: 'Annulla'
});
if (!result.isConfirmed) return;
showToast('Creazione backup in corso...', 'info');
const response = await apiCall(`/api/vm/${vmId}/backup`, 'POST');
if (response.status === 'success') {
showToast('Backup avviato con successo!', 'success');
} else {
showAlert('Errore: ' + response.message, 'error');
}
}
async function deleteBackup(node, storage, filename, vmId) {
const result = await Swal.fire({
title: 'Elimina Backup',
text: `Vuoi eliminare il backup "${filename}"? Questa operazione non può essere annullata.`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Elimina',
cancelButtonText: 'Annulla',
confirmButtonColor: '#f85149'
});
if (!result.isConfirmed) return;
showToast('Eliminazione backup in corso...', 'info');
const response = await apiCall(`/api/backup/${node}/${storage}/${filename}/delete`, 'DELETE');
if (response.status === 'success') {
showToast('Backup eliminato con successo!', 'success');
const vmName = document.getElementById('backupModalTitle').textContent.split(':')[1].split('(')[0].trim();
showBackups(vmId, vmName);
} else {
showAlert('Errore: ' + response.message, 'error');
}
}
function closeBackupModal() {
document.getElementById('backupModal').style.display = 'none';
}
// ==================== SNAPSHOTS ====================
async function showSnapshots(vmId, vmName) {
const modal = document.getElementById('snapshotModal');
const title = document.getElementById('snapshotModalTitle');
const content = document.getElementById('snapshotModalContent');
title.textContent = `Snapshot: ${vmName} (VM ${vmId})`;
content.innerHTML = '<div class="loading"><div class="spinner"></div><p>Caricamento snapshot...</p></div>';
modal.style.display = 'block';
const result = await apiCall(`/api/vm/${vmId}/snapshots`);
if (result.status === 'success') {
if (result.data.length === 0) {
content.innerHTML = `
<p class="text-muted text-center" style="padding: var(--space-xl);">Nessuna snapshot disponibile</p>
<div style="text-align: center;">
<button class="btn btn-primary" onclick="promptCreateSnapshot(${vmId})">Crea Snapshot</button>
</div>
`;
} else {
content.innerHTML = `
<p class="text-muted mb-2">Trovate ${result.count} snapshot (max 3 consentite)</p>
<div class="snapshot-list">
${result.data.map(snapshot => {
const snapTime = snapshot.snaptime ? formatDate(snapshot.snaptime) : 'N/A';
return `
<div class="snapshot-item">
<div class="snapshot-info">
<div class="snapshot-name">${snapshot.name}</div>
<div class="snapshot-date">${snapTime}</div>
${snapshot.description ? `<div class="text-muted" style="font-size: 0.8rem;">${snapshot.description}</div>` : ''}
</div>
<button class="btn btn-danger btn-sm"
onclick="deleteSnapshot(${vmId}, '${snapshot.name}', '${vmName}')">
Elimina
</button>
</div>
`;
}).join('')}
</div>
<div style="text-align: center; margin-top: var(--space-lg);">
${result.count < 3 ? `
<button class="btn btn-primary" onclick="promptCreateSnapshot(${vmId})">Crea Nuova Snapshot</button>
` : `
<p class="text-danger">Limite di 3 snapshot raggiunto. Elimina una snapshot per crearne una nuova.</p>
`}
</div>
`;
}
} else {
content.innerHTML = `<div class="alert alert-error">${result.message}</div>`;
}
}
async function promptCreateSnapshot(vmId) {
const { value: formValues } = await Swal.fire({
title: 'Crea Snapshot',
html: `
<div class="form-group" style="text-align: left;">
<label>Nome (opzionale)</label>
<input id="swal-snapname" class="swal2-input" placeholder="auto-generato se vuoto">
</div>
<div class="form-group" style="text-align: left;">
<label>Descrizione (opzionale)</label>
<input id="swal-snapdesc" class="swal2-input" placeholder="Descrizione snapshot">
</div>
`,
focusConfirm: false,
showCancelButton: true,
confirmButtonText: 'Crea',
cancelButtonText: 'Annulla',
preConfirm: () => {
return {
snapname: document.getElementById('swal-snapname').value,
description: document.getElementById('swal-snapdesc').value
};
}
});
if (formValues) {
await createSnapshot(vmId, formValues.snapname || undefined, formValues.description || undefined);
}
}
async function createSnapshot(vmId, snapname, description) {
showToast('Creazione snapshot in corso...', 'info');
const data = {};
if (snapname) data.snapname = snapname;
if (description) data.description = description;
const result = await apiCall(`/api/vm/${vmId}/snapshot`, 'POST', data);
if (result.status === 'success') {
showToast('Snapshot creata con successo!', 'success');
const vmName = document.getElementById('snapshotModalTitle').textContent.split(':')[1].split('(')[0].trim();
showSnapshots(vmId, vmName);
} else {
showAlert('Errore: ' + result.message, 'error');
}
}
async function deleteSnapshot(vmId, snapname, vmName) {
const result = await Swal.fire({
title: 'Elimina Snapshot',
text: `Vuoi eliminare la snapshot "${snapname}"? Questa operazione non può essere annullata.`,
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Elimina',
cancelButtonText: 'Annulla',
confirmButtonColor: '#f85149'
});
if (!result.isConfirmed) return;
showToast('Eliminazione snapshot in corso...', 'info');
const response = await apiCall(`/api/vm/${vmId}/snapshot/${snapname}`, 'DELETE');
if (response.status === 'success') {
showToast('Snapshot eliminata con successo!', 'success');
showSnapshots(vmId, vmName);
} else {
showAlert('Errore: ' + response.message, 'error');
}
}
function closeSnapshotModal() {
document.getElementById('snapshotModal').style.display = 'none';
}
// ==================== MODAL CLOSE ON OUTSIDE CLICK ====================
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
// Cleanup console if closing console modal
if (event.target.id === 'consoleModal') {
document.getElementById('consoleContainer').innerHTML = '';
}
}
}
</script>
{% endblock %}