860 lines
33 KiB
HTML
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()">×</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()">×</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()">×</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()">×</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 %}
|