dashboard.js 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062
  1. const API_BASE = window.location.origin;
  2. let selectedVM = null;
  3. // Initialize dashboard
  4. document.addEventListener('DOMContentLoaded', () => {
  5. loadVMs();
  6. loadISOsForSelect();
  7. // Initialize sidebar state for mobile
  8. const sidebar = document.querySelector('.sidebar');
  9. if (window.innerWidth <= 768) {
  10. sidebar.classList.add('hidden-mobile');
  11. }
  12. // Create VM Form
  13. document.getElementById('createVMForm').addEventListener('submit', createVM);
  14. });
  15. // Get badge class based on VM state
  16. function getStateClass(state) {
  17. switch (state) {
  18. case 'running': return 'badge-running';
  19. case 'shut off': return 'badge-off';
  20. case 'paused': return 'badge-paused';
  21. case 'crashed': return 'badge-error';
  22. default: return 'bg-light text-dark';
  23. }
  24. }
  25. // Get icon for state
  26. function getStateIcon(state) {
  27. switch (state) {
  28. case 'running': return '<i class="bi bi-play-circle-fill"></i>';
  29. case 'shut off': return '<i class="bi bi-power"></i>';
  30. case 'paused': return '<i class="bi bi-pause-circle-fill"></i>';
  31. default: return '<i class="bi bi-question-circle"></i>';
  32. }
  33. }
  34. // Load VMs from API
  35. async function loadVMs() {
  36. try {
  37. const response = await fetch(`${API_BASE}/vms`);
  38. const data = await response.json();
  39. const vmList = document.getElementById('vmList');
  40. if (!data.vms || data.vms.length === 0) {
  41. vmList.innerHTML = '<p style="text-align: center; color: rgba(255,255,255,0.7); margin-top: 20px;">No hay máquinas virtuales</p>';
  42. return;
  43. }
  44. vmList.innerHTML = data.vms.map(vm => `
  45. <div class="vm-item ${selectedVM === vm.name ? 'active' : ''}" onclick="selectVM('${vm.name}')">
  46. <div class="vm-item-name">
  47. <div>${vm.name}</div>
  48. <small style="opacity: 0.9;">${vm.id !== -1 ? 'ID: ' + vm.id : 'Detenida'}</small>
  49. </div>
  50. <span class="vm-item-badge badge ${getStateClass(vm.state)}">${vm.state}</span>
  51. </div>
  52. `).join('');
  53. } catch (error) {
  54. console.error('Error loading VMs:', error);
  55. }
  56. }
  57. // Select a VM and show details
  58. async function selectVM(vmName) {
  59. selectedVM = vmName;
  60. loadVMs(); // Refresh to highlight selected
  61. // Clean up previous charts
  62. cleanupCharts();
  63. try {
  64. const response = await fetch(`${API_BASE}/vms/${vmName}/info`);
  65. const vm = await response.json();
  66. if (vm.error) {
  67. showError(vm.error);
  68. return;
  69. }
  70. displayVMDetails(vm, vmName);
  71. // Initialize performance charts only if VM is running
  72. if (vm.state === 'running') {
  73. setTimeout(() => initializePerformanceCharts(vmName), 100);
  74. }
  75. } catch (error) {
  76. console.error('Error loading VM info:', error);
  77. showError('Error al cargar información de la VM');
  78. }
  79. }
  80. // Display VM details
  81. function displayVMDetails(vm, vmName) {
  82. const contentArea = document.getElementById('contentArea');
  83. const topBar = document.getElementById('currentVMName');
  84. const topBarActions = document.getElementById('topBarActions');
  85. topBar.textContent = vmName;
  86. // Build status badge
  87. const statusBadge = `<span class="badge ${getStateClass(vm.state)} p-2">${vm.state}</span>`;
  88. // Build action buttons
  89. const actionButtons = `
  90. <div>
  91. ${vm.state === 'running' ? `
  92. <button class="btn btn-sm btn-warning" onclick="stopVM('${vmName}')">
  93. <i class="bi bi-stop-circle"></i> Detener
  94. </button>
  95. <button class="btn btn-sm btn-info" onclick="openConsole('${vmName}')">
  96. <i class="bi bi-display"></i> Consola
  97. </button>
  98. ` : `
  99. <button class="btn btn-sm btn-success" onclick="startVM('${vmName}')">
  100. <i class="bi bi-play-circle"></i> Iniciar
  101. </button>
  102. `}
  103. <button class="btn btn-sm btn-danger" onclick="deleteVMConfirm('${vmName}')">
  104. <i class="bi bi-trash"></i> Eliminar
  105. </button>
  106. </div>
  107. `;
  108. topBarActions.innerHTML = actionButtons;
  109. // Build content
  110. const disksHTML = vm.disks && vm.disks.length > 0 ? vm.disks.map(disk => `
  111. <li>
  112. <strong>${disk.target}</strong> (${disk.driver}) - ${disk.source}
  113. </li>
  114. `).join('') : '<li>Sin discos</li>';
  115. const interfacesHTML = vm.interfaces && vm.interfaces.length > 0 ? vm.interfaces.map(iface => `
  116. <li>
  117. <strong>MAC:</strong> ${iface.mac}<br>
  118. <strong>Red:</strong> ${iface.network}
  119. </li>
  120. `).join('') : '<li>Sin interfaces</li>';
  121. contentArea.innerHTML = `
  122. <ul class="nav nav-tabs" role="tablist">
  123. <li class="nav-item">
  124. <a class="nav-link active" id="overview-tab" data-bs-toggle="tab" href="#overview">Resumen</a>
  125. </li>
  126. <li class="nav-item">
  127. <a class="nav-link" id="disks-tab" data-bs-toggle="tab" href="#disks">Discos</a>
  128. </li>
  129. <li class="nav-item">
  130. <a class="nav-link" id="network-tab" data-bs-toggle="tab" href="#network">Red</a>
  131. </li>
  132. </ul>
  133. <div class="tab-content">
  134. <!-- Overview Tab -->
  135. <div class="tab-pane fade show active" id="overview">
  136. <div class="vm-details">
  137. <div class="card">
  138. <div class="card-header">
  139. <h6 class="card-title mb-0"><i class="bi bi-gear"></i> Configuración</h6>
  140. </div>
  141. <div class="card-body">
  142. <div class="info-item">
  143. <div class="info-label">ID de Dominio</div>
  144. <div class="info-value">${vm.id !== -1 ? vm.id : 'N/A (Parada)'}</div>
  145. </div>
  146. <div class="info-item">
  147. <div class="info-label">Estado</div>
  148. <div class="info-value">${statusBadge}</div>
  149. </div>
  150. <div class="info-item">
  151. <div class="info-label">Procesadores</div>
  152. <div class="info-value">${vm.cpus} CPU${vm.cpus !== 1 ? 's' : ''}</div>
  153. </div>
  154. <div class="info-item">
  155. <div class="info-label">Memoria RAM</div>
  156. <div class="info-value">${vm.memory} MiB</div>
  157. </div>
  158. </div>
  159. </div>
  160. <div class="card">
  161. <div class="card-header">
  162. <h6 class="card-title mb-0"><i class="bi bi-display"></i> Display</h6>
  163. </div>
  164. <div class="card-body">
  165. <div class="info-item">
  166. <div class="info-label">Puerto VNC</div>
  167. <div class="info-value">${vm.vnc_port ? vm.vnc_port : 'No asignado'}</div>
  168. </div>
  169. <div class="info-item">
  170. <div class="info-label">Dirección</div>
  171. <div class="info-value">${vm.vnc_port ? `localhost:${vm.vnc_port}` : 'N/A'}</div>
  172. </div>
  173. </div>
  174. </div>
  175. <!-- Performance Indicators -->
  176. ${vm.state === 'running' ? `
  177. <div class="card">
  178. <div class="card-header">
  179. <h6 class="card-title mb-0"><i class="bi bi-graph-up"></i> Rendimiento</h6>
  180. </div>
  181. <div class="card-body">
  182. <div class="mb-3">
  183. <div class="d-flex justify-content-between mb-1">
  184. <strong><i class="bi bi-cpu"></i> CPU</strong>
  185. <span id="cpuPercent">0%</span>
  186. </div>
  187. <div class="progress" style="height: 25px;">
  188. <div id="cpuBar" class="progress-bar bg-primary" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
  189. </div>
  190. </div>
  191. <div>
  192. <div class="d-flex justify-content-between mb-1">
  193. <strong><i class="bi bi-memory"></i> Memoria</strong>
  194. <span id="memPercent">0%</span>
  195. </div>
  196. <div class="progress" style="height: 25px;">
  197. <div id="memBar" class="progress-bar bg-warning" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
  198. </div>
  199. </div>
  200. </div>
  201. </div>
  202. ` : ''}
  203. </div>
  204. <div class="actions-group">
  205. <h5><i class="bi bi-lightning"></i> Acciones</h5>
  206. <button class="btn btn-sm btn-primary btn-action" onclick="openBootOrderModal('${vmName}')">
  207. <i class="bi bi-arrow-repeat"></i> Configurar Arranque
  208. </button>
  209. <button class="btn btn-sm btn-info btn-action" onclick="openAddDiskModal('${vmName}')" ${vm.state !== 'shut off' ? 'disabled' : ''}>
  210. <i class="bi bi-plus-circle"></i> Agregar Disco
  211. </button>
  212. <button class="btn btn-sm btn-warning btn-action" onclick="openSnapshotModal('${vmName}')">
  213. <i class="bi bi-camera"></i> Crear Snapshot
  214. </button>
  215. <button class="btn btn-sm btn-secondary btn-action" onclick="openSnapshotsListModal('${vmName}')">
  216. <i class="bi bi-collection"></i> Ver Snapshots
  217. </button>
  218. <button class="btn btn-sm btn-secondary btn-action" onclick="downloadVMConfig('${vmName}')">
  219. <i class="bi bi-download"></i> Descargar XML
  220. </button>
  221. </div>
  222. </div>
  223. <!-- Disks Tab -->
  224. <div class="tab-pane fade" id="disks">
  225. <div class="card">
  226. <div class="card-header">
  227. <h6 class="card-title mb-0"><i class="bi bi-hdd"></i> Discos</h6>
  228. </div>
  229. <div class="card-body">
  230. <ul class="list-unstyled">
  231. ${disksHTML}
  232. </ul>
  233. </div>
  234. </div>
  235. </div>
  236. <!-- Network Tab -->
  237. <div class="tab-pane fade" id="network">
  238. <div class="card">
  239. <div class="card-header">
  240. <h6 class="card-title mb-0"><i class="bi bi-diagram-3"></i> Interfaces de Red</h6>
  241. </div>
  242. <div class="card-body">
  243. <ul class="list-unstyled">
  244. ${interfacesHTML}
  245. </ul>
  246. </div>
  247. </div>
  248. </div>
  249. </div>
  250. `;
  251. }
  252. // Show error message
  253. function showError(message) {
  254. const contentArea = document.getElementById('contentArea');
  255. contentArea.innerHTML = `
  256. <div class="alert alert-danger" role="alert">
  257. <i class="bi bi-exclamation-circle"></i> ${message}
  258. </div>
  259. `;
  260. }
  261. // Open Create VM Modal
  262. function openCreateVMModal() {
  263. const modal = new bootstrap.Modal(document.getElementById('createVMModal'));
  264. modal.show();
  265. }
  266. // Create VM
  267. async function createVM(e) {
  268. e.preventDefault();
  269. const name = document.getElementById('name').value;
  270. const memory = document.getElementById('memory').value;
  271. const disk_size = document.getElementById('disk_size').value;
  272. const cpus = document.getElementById('cpus').value;
  273. const iso = document.getElementById('iso').value;
  274. if (!iso) {
  275. alert('Por favor selecciona una ISO');
  276. return;
  277. }
  278. try {
  279. const response = await fetch(`${API_BASE}/vms?name=${name}&memory=${memory}&disk_size=${disk_size}&cpus=${cpus}&iso=${iso}`, {
  280. method: 'POST'
  281. });
  282. let result;
  283. try {
  284. result = await response.json();
  285. } catch {
  286. result = { error: `Server error: ${response.status}` };
  287. }
  288. if (response.ok) {
  289. alert(result.message || 'VM creada correctamente');
  290. document.getElementById('createVMForm').reset();
  291. bootstrap.Modal.getInstance(document.getElementById('createVMModal')).hide();
  292. loadVMs();
  293. } else {
  294. alert(result.error || 'Error al crear VM');
  295. }
  296. } catch (error) {
  297. console.error('Error:', error);
  298. alert(`Error: ${error.message}`);
  299. }
  300. }
  301. // Load ISOs for select
  302. async function loadISOsForSelect() {
  303. try {
  304. const response = await fetch(`${API_BASE}/isos`);
  305. const data = await response.json();
  306. const select = document.getElementById('iso');
  307. select.innerHTML = '<option value="">Selecciona una ISO</option>';
  308. if (data.isos && data.isos.length > 0) {
  309. data.isos.forEach(iso => {
  310. select.innerHTML += `<option value="${iso.path}">${iso.name}</option>`;
  311. });
  312. }
  313. } catch (error) {
  314. console.error('Error loading ISOs:', error);
  315. }
  316. }
  317. // Start VM
  318. async function startVM(vmName) {
  319. try {
  320. const response = await fetch(`${API_BASE}/vms/${vmName}/start`);
  321. const result = await response.json();
  322. if (response.ok) {
  323. loadVMs();
  324. if (selectedVM === vmName) {
  325. selectVM(vmName);
  326. }
  327. } else {
  328. alert(result.error || 'Error al iniciar VM');
  329. }
  330. } catch (error) {
  331. console.error('Error:', error);
  332. alert(`Error: ${error.message}`);
  333. }
  334. }
  335. // Stop VM
  336. async function stopVM(vmName) {
  337. if (confirm(`¿Estás seguro de que quieres detener ${vmName}?`)) {
  338. try {
  339. const response = await fetch(`${API_BASE}/vms/${vmName}/stop`);
  340. const result = await response.json();
  341. if (response.ok) {
  342. loadVMs();
  343. if (selectedVM === vmName) {
  344. selectVM(vmName);
  345. }
  346. } else {
  347. alert(result.error || 'Error al detener VM');
  348. }
  349. } catch (error) {
  350. console.error('Error:', error);
  351. alert(`Error: ${error.message}`);
  352. }
  353. }
  354. }
  355. // Delete VM
  356. async function deleteVMConfirm(vmName) {
  357. if (confirm(`¿Estás seguro de que quieres eliminar ${vmName}? Esta acción no se puede deshacer.`)) {
  358. try {
  359. const response = await fetch(`${API_BASE}/vms/${vmName}`, { method: 'DELETE' });
  360. const result = await response.json();
  361. if (response.ok) {
  362. alert('VM eliminada correctamente');
  363. selectedVM = null;
  364. loadVMs();
  365. document.getElementById('contentArea').innerHTML = `
  366. <div class="empty-state">
  367. <i class="bi bi-inbox"></i>
  368. <h5>Selecciona una máquina virtual</h5>
  369. <p>Haz clic en una VM del panel lateral para ver sus detalles</p>
  370. </div>
  371. `;
  372. } else {
  373. alert(result.error || 'Error al eliminar VM');
  374. }
  375. } catch (error) {
  376. console.error('Error:', error);
  377. alert(`Error: ${error.message}`);
  378. }
  379. }
  380. }
  381. // Open Boot Order Modal
  382. function openBootOrderModal(vmName) {
  383. const html = `
  384. <div class="modal fade" id="bootOrderModal" tabindex="-1">
  385. <div class="modal-dialog">
  386. <div class="modal-content">
  387. <div class="modal-header">
  388. <h5 class="modal-title">Configurar Orden de Arranque - ${vmName}</h5>
  389. <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
  390. </div>
  391. <form id="bootOrderForm">
  392. <div class="modal-body">
  393. <div class="mb-3">
  394. <label for="bootOrderInput" class="form-label">Orden de Arranque</label>
  395. <input type="text" class="form-control" id="bootOrderInput" placeholder="hd,cdrom" required>
  396. <small class="form-text text-muted">Separados por comas: hd, cdrom, network</small>
  397. </div>
  398. </div>
  399. <div class="modal-footer">
  400. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
  401. <button type="submit" class="btn btn-primary">Guardar</button>
  402. </div>
  403. </form>
  404. </div>
  405. </div>
  406. </div>
  407. `;
  408. document.body.insertAdjacentHTML('beforeend', html);
  409. const modal = new bootstrap.Modal(document.getElementById('bootOrderModal'));
  410. document.getElementById('bootOrderForm').addEventListener('submit', async (e) => {
  411. e.preventDefault();
  412. const order = document.getElementById('bootOrderInput').value.split(',').map(s => s.trim());
  413. try {
  414. const response = await fetch(`${API_BASE}/vms/${vmName}/boot-order`, {
  415. method: 'PUT',
  416. headers: { 'Content-Type': 'application/json' },
  417. body: JSON.stringify({ order })
  418. });
  419. const result = await response.json();
  420. if (response.ok) {
  421. alert('Orden de arranque actualizado');
  422. modal.hide();
  423. document.getElementById('bootOrderModal').remove();
  424. } else {
  425. alert(result.error || 'Error al actualizar');
  426. }
  427. } catch (error) {
  428. alert(`Error: ${error.message}`);
  429. }
  430. });
  431. modal.show();
  432. }
  433. // Open Add Disk Modal
  434. function openAddDiskModal(vmName) {
  435. const html = `
  436. <div class="modal fade" id="addDiskModal" tabindex="-1">
  437. <div class="modal-dialog">
  438. <div class="modal-content">
  439. <div class="modal-header">
  440. <h5 class="modal-title">Agregar Disco - ${vmName}</h5>
  441. <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
  442. </div>
  443. <div class="alert alert-warning m-3">
  444. <small><strong>⚠️ Importante:</strong> La VM debe estar parada y el nombre del disco no debe existir</small>
  445. </div>
  446. <form id="addDiskFormModal">
  447. <div class="modal-body">
  448. <div class="mb-3">
  449. <label for="diskNameInput" class="form-label">Nombre del Disco</label>
  450. <input type="text" class="form-control" id="diskNameInput" placeholder="datos" required>
  451. </div>
  452. <div class="mb-3">
  453. <label for="diskSizeInput" class="form-label">Tamaño (GB)</label>
  454. <input type="number" class="form-control" id="diskSizeInput" value="10" min="1" required>
  455. </div>
  456. </div>
  457. <div class="modal-footer">
  458. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
  459. <button type="submit" class="btn btn-primary">Agregar</button>
  460. </div>
  461. </form>
  462. </div>
  463. </div>
  464. </div>
  465. `;
  466. document.body.insertAdjacentHTML('beforeend', html);
  467. const modal = new bootstrap.Modal(document.getElementById('addDiskModal'));
  468. document.getElementById('addDiskFormModal').addEventListener('submit', async (e) => {
  469. e.preventDefault();
  470. const disk_name = document.getElementById('diskNameInput').value;
  471. const disk_size = document.getElementById('diskSizeInput').value;
  472. try {
  473. const response = await fetch(`${API_BASE}/vms/${vmName}/add-disk?disk_name=${disk_name}&disk_size=${disk_size}`, {
  474. method: 'POST'
  475. });
  476. let result;
  477. try {
  478. result = await response.json();
  479. } catch {
  480. result = { error: 'Error del servidor' };
  481. }
  482. if (response.ok) {
  483. alert(result.message || 'Disco agregado correctamente');
  484. modal.hide();
  485. document.getElementById('addDiskModal').remove();
  486. selectVM(vmName); // Refresh
  487. } else {
  488. alert(result.error || 'Error al agregar disco');
  489. }
  490. } catch (error) {
  491. alert(`Error: ${error.message}`);
  492. }
  493. });
  494. modal.show();
  495. }
  496. // Open Console
  497. function openConsole(vmName) {
  498. fetch(`${API_BASE}/console/${vmName}`)
  499. .then(response => response.json())
  500. .then(data => {
  501. if (data.url) {
  502. window.open(data.url);
  503. } else {
  504. alert(data.error || 'Error al abrir consola');
  505. }
  506. })
  507. .catch(error => {
  508. console.error('Error:', error);
  509. alert('Error al abrir consola');
  510. });
  511. }
  512. // Open ISO Management Modal
  513. function openISOManagementModal() {
  514. const modal = new bootstrap.Modal(document.getElementById('isoManagementModal'));
  515. loadISOs();
  516. setupUploadISOForm();
  517. modal.show();
  518. }
  519. // Load ISOs for management
  520. async function loadISOs() {
  521. try {
  522. const response = await fetch(`${API_BASE}/isos`);
  523. const data = await response.json();
  524. const tbody = document.getElementById('isoTableBody');
  525. if (!data.isos || data.isos.length === 0) {
  526. tbody.innerHTML = '<tr><td colspan="2" class="text-center text-muted">No hay ISOs subidas</td></tr>';
  527. return;
  528. }
  529. tbody.innerHTML = data.isos.map(iso => `
  530. <tr>
  531. <td>
  532. <i class="bi bi-disc"></i> ${iso.name}
  533. </td>
  534. <td>
  535. <button class="btn btn-sm btn-danger" onclick="deleteISO('${iso.name}')">
  536. <i class="bi bi-trash"></i> Eliminar
  537. </button>
  538. </td>
  539. </tr>
  540. `).join('');
  541. } catch (error) {
  542. console.error('Error loading ISOs:', error);
  543. document.getElementById('isoTableBody').innerHTML = '<tr><td colspan="2" class="text-center text-danger">Error al cargar ISOs</td></tr>';
  544. }
  545. }
  546. // Setup ISO upload form
  547. function setupUploadISOForm() {
  548. const form = document.getElementById('uploadIsoForm');
  549. if (!form.dataset.listenerAdded) {
  550. form.addEventListener('submit', uploadISO);
  551. form.dataset.listenerAdded = 'true';
  552. }
  553. }
  554. // Upload ISO
  555. async function uploadISO(e) {
  556. e.preventDefault();
  557. const isoFile = document.getElementById('isoFile');
  558. const uploadBtn = document.getElementById('uploadBtn');
  559. const progressContainer = document.getElementById('progressContainer');
  560. const uploadProgress = document.getElementById('uploadProgress');
  561. if (!isoFile.files.length) {
  562. alert('Por favor selecciona un archivo');
  563. return;
  564. }
  565. const formData = new FormData();
  566. formData.append('file', isoFile.files[0]);
  567. uploadBtn.disabled = true;
  568. progressContainer.style.display = 'block';
  569. try {
  570. const xhr = new XMLHttpRequest();
  571. xhr.upload.addEventListener('progress', (e) => {
  572. if (e.lengthComputable) {
  573. const percentComplete = (e.loaded / e.total) * 100;
  574. uploadProgress.style.width = percentComplete + '%';
  575. uploadProgress.setAttribute('aria-valuenow', percentComplete);
  576. uploadProgress.textContent = Math.round(percentComplete) + '%';
  577. }
  578. });
  579. xhr.addEventListener('load', () => {
  580. if (xhr.status === 200) {
  581. const result = JSON.parse(xhr.responseText);
  582. alert(result.message || 'ISO subida correctamente');
  583. document.getElementById('uploadIsoForm').reset();
  584. progressContainer.style.display = 'none';
  585. uploadProgress.style.width = '0%';
  586. uploadProgress.textContent = '0%';
  587. loadISOs();
  588. loadISOsForSelect();
  589. } else {
  590. const result = JSON.parse(xhr.responseText);
  591. alert(result.error || 'Error al subir ISO');
  592. progressContainer.style.display = 'none';
  593. }
  594. uploadBtn.disabled = false;
  595. });
  596. xhr.addEventListener('error', () => {
  597. alert('Error al subir ISO');
  598. progressContainer.style.display = 'none';
  599. uploadBtn.disabled = false;
  600. });
  601. xhr.open('POST', `${API_BASE}/upload-iso`);
  602. xhr.send(formData);
  603. } catch (error) {
  604. console.error('Error:', error);
  605. alert(`Error: ${error.message}`);
  606. uploadBtn.disabled = false;
  607. progressContainer.style.display = 'none';
  608. }
  609. }
  610. // Delete ISO
  611. async function deleteISO(name) {
  612. if (confirm(`¿Estás seguro de que quieres eliminar ${name}?`)) {
  613. try {
  614. const response = await fetch(`${API_BASE}/isos/${name}`, { method: 'DELETE' });
  615. const result = await response.json();
  616. if (response.ok) {
  617. alert('ISO eliminada correctamente');
  618. loadISOs();
  619. loadISOsForSelect();
  620. } else {
  621. alert(result.error || 'Error al eliminar ISO');
  622. }
  623. } catch (error) {
  624. console.error('Error:', error);
  625. alert(`Error: ${error.message}`);
  626. }
  627. }
  628. }
  629. // Mobile responsive - Toggle sidebar
  630. function toggleSidebar() {
  631. const sidebar = document.querySelector('.sidebar');
  632. // Para móviles (max-width 768px), mostrar/ocultar el sidebar
  633. if (window.innerWidth <= 768) {
  634. sidebar.classList.toggle('show');
  635. }
  636. }
  637. // Cerrar sidebar cuando se selecciona una VM en móvil
  638. const originalSelectVM = window.selectVM;
  639. window.selectVM = function(vmName) {
  640. originalSelectVM(vmName);
  641. // Cerrar sidebar en móvil después de seleccionar
  642. if (window.innerWidth <= 768) {
  643. const sidebar = document.querySelector('.sidebar');
  644. sidebar.classList.remove('show');
  645. }
  646. };
  647. // Cerrar sidebar cuando se abre un modal
  648. document.addEventListener('shown.bs.modal', function() {
  649. if (window.innerWidth <= 768) {
  650. const sidebar = document.querySelector('.sidebar');
  651. sidebar.classList.remove('show');
  652. }
  653. });
  654. // Download VM configuration as XML
  655. async function downloadVMConfig(vmName) {
  656. try {
  657. const response = await fetch(`${API_BASE}/vms/${vmName}/config`);
  658. if (!response.ok) {
  659. alert(`Error: El servidor respondió con estado ${response.status}`);
  660. return;
  661. }
  662. const data = await response.json();
  663. if (data.error) {
  664. alert(`Error: ${data.error}`);
  665. return;
  666. }
  667. // Validar que tenemos el contenido XML
  668. if (!data.xml || data.xml === undefined || data.xml === 'undefined') {
  669. console.error('XML content is undefined or missing:', data);
  670. alert('Error: No se pudo obtener la configuración XML');
  671. return;
  672. }
  673. // Create a blob with the XML content
  674. const blob = new Blob([data.xml], { type: 'application/xml' });
  675. const url = window.URL.createObjectURL(blob);
  676. const link = document.createElement('a');
  677. link.href = url;
  678. link.download = `${vmName}-config.xml`;
  679. document.body.appendChild(link);
  680. link.click();
  681. document.body.removeChild(link);
  682. window.URL.revokeObjectURL(url);
  683. } catch (error) {
  684. console.error('Error downloading config:', error);
  685. alert(`Error al descargar configuración: ${error.message}`);
  686. }
  687. }
  688. // Open snapshot modal
  689. function openSnapshotModal(vmName) {
  690. const modal = document.createElement('div');
  691. modal.classList.add('modal', 'fade');
  692. modal.innerHTML = `
  693. <div class="modal-dialog">
  694. <div class="modal-content">
  695. <div class="modal-header">
  696. <h5 class="modal-title">Crear Snapshot - ${vmName}</h5>
  697. <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
  698. </div>
  699. <div class="modal-body">
  700. <div class="mb-3">
  701. <label for="snapshotName" class="form-label">Nombre del Snapshot</label>
  702. <input type="text" class="form-control" id="snapshotName" placeholder="ej: snapshot-2026-01-18" required>
  703. </div>
  704. <div class="mb-3">
  705. <label for="snapshotDesc" class="form-label">Descripción (opcional)</label>
  706. <textarea class="form-control" id="snapshotDesc" rows="3" placeholder="Describe el propósito de este snapshot"></textarea>
  707. </div>
  708. </div>
  709. <div class="modal-footer">
  710. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
  711. <button type="button" class="btn btn-warning" onclick="createSnapshot('${vmName}')">
  712. <i class="bi bi-camera"></i> Crear Snapshot
  713. </button>
  714. </div>
  715. </div>
  716. </div>
  717. `;
  718. document.body.appendChild(modal);
  719. const bsModal = new bootstrap.Modal(modal);
  720. bsModal.show();
  721. // Limpiar modal al cerrarlo
  722. modal.addEventListener('hidden.bs.modal', () => {
  723. modal.remove();
  724. });
  725. }
  726. // Create snapshot
  727. async function createSnapshot(vmName) {
  728. const snapshotName = document.getElementById('snapshotName').value.trim();
  729. const snapshotDesc = document.getElementById('snapshotDesc').value.trim();
  730. if (!snapshotName) {
  731. alert('El nombre del snapshot es obligatorio');
  732. return;
  733. }
  734. // Validar que el nombre sea válido (sin espacios ni caracteres especiales)
  735. if (!/^[a-zA-Z0-9._-]+$/.test(snapshotName)) {
  736. alert('El nombre del snapshot solo puede contener letras, números, puntos, guiones y guiones bajos');
  737. return;
  738. }
  739. try {
  740. const url = new URL(`${API_BASE}/vms/${vmName}/snapshot`);
  741. url.searchParams.append('snapshot_name', snapshotName);
  742. if (snapshotDesc) {
  743. url.searchParams.append('description', snapshotDesc);
  744. }
  745. const response = await fetch(url, { method: 'POST' });
  746. const data = await response.json();
  747. if (data.error) {
  748. alert(`Error: ${data.error}`);
  749. return;
  750. }
  751. alert(`✓ Snapshot "${snapshotName}" creado exitosamente`);
  752. // Cerrar modal - buscar el modal visible
  753. const visibleModal = document.querySelector('.modal.show');
  754. if (visibleModal) {
  755. const modalInstance = bootstrap.Modal.getInstance(visibleModal);
  756. if (modalInstance) {
  757. modalInstance.hide();
  758. }
  759. }
  760. } catch (error) {
  761. console.error('Error:', error);
  762. alert(`Error al crear snapshot: ${error.message}`);
  763. }
  764. }
  765. // Open snapshots list modal
  766. function openSnapshotsListModal(vmName) {
  767. const modal = document.createElement('div');
  768. modal.classList.add('modal', 'fade');
  769. modal.innerHTML = `
  770. <div class="modal-dialog modal-lg">
  771. <div class="modal-content">
  772. <div class="modal-header">
  773. <h5 class="modal-title">Snapshots - ${vmName}</h5>
  774. <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
  775. </div>
  776. <div class="modal-body">
  777. <div id="snapshotsList" class="text-center">
  778. <p class="text-muted">Cargando snapshots...</p>
  779. </div>
  780. </div>
  781. <div class="modal-footer">
  782. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
  783. </div>
  784. </div>
  785. </div>
  786. `;
  787. document.body.appendChild(modal);
  788. const bsModal = new bootstrap.Modal(modal);
  789. bsModal.show();
  790. // Limpiar modal al cerrarlo
  791. modal.addEventListener('hidden.bs.modal', () => {
  792. modal.remove();
  793. });
  794. // Cargar snapshots
  795. loadSnapshotsList(vmName);
  796. }
  797. // Load and display snapshots list
  798. async function loadSnapshotsList(vmName) {
  799. try {
  800. const response = await fetch(`${API_BASE}/vms/${vmName}/snapshots`);
  801. const data = await response.json();
  802. if (data.error) {
  803. document.getElementById('snapshotsList').innerHTML = `<p class="text-danger">Error: ${data.error}</p>`;
  804. return;
  805. }
  806. const snapshots = data.snapshots;
  807. if (!snapshots || snapshots.length === 0) {
  808. document.getElementById('snapshotsList').innerHTML = '<p class="text-muted">No hay snapshots disponibles</p>';
  809. return;
  810. }
  811. let html = `
  812. <div class="table-responsive">
  813. <table class="table table-sm table-hover">
  814. <thead>
  815. <tr>
  816. <th>Nombre</th>
  817. <th>Creado</th>
  818. <th>Descripción</th>
  819. <th>Estado</th>
  820. <th>Acciones</th>
  821. </tr>
  822. </thead>
  823. <tbody>
  824. `;
  825. snapshots.forEach(snap => {
  826. let created = 'Desconocida';
  827. if (snap.created && snap.created > 0) {
  828. created = new Date(snap.created * 1000).toLocaleString('es-ES');
  829. }
  830. html += `
  831. <tr>
  832. <td><strong>${snap.name}</strong></td>
  833. <td>${created}</td>
  834. <td>${snap.description || '-'}</td>
  835. <td><span class="badge bg-info">${snap.state}</span></td>
  836. <td>
  837. <button class="btn btn-sm btn-danger" onclick="deleteSnapshotConfirm('${vmName}', '${snap.name}')">
  838. <i class="bi bi-trash"></i> Eliminar
  839. </button>
  840. </td>
  841. </tr>
  842. `;
  843. });
  844. html += `
  845. </tbody>
  846. </table>
  847. </div>
  848. `;
  849. document.getElementById('snapshotsList').innerHTML = html;
  850. } catch (error) {
  851. console.error('Error:', error);
  852. document.getElementById('snapshotsList').innerHTML = `<p class="text-danger">Error: ${error.message}</p>`;
  853. }
  854. }
  855. // Delete snapshot with confirmation
  856. function deleteSnapshotConfirm(vmName, snapshotName) {
  857. if (confirm(`¿Estás seguro de que deseas eliminar el snapshot "${snapshotName}"?`)) {
  858. deleteSnapshot(vmName, snapshotName);
  859. }
  860. }
  861. // Delete snapshot
  862. async function deleteSnapshot(vmName, snapshotName) {
  863. try {
  864. const encodedSnapshotName = encodeURIComponent(snapshotName);
  865. const response = await fetch(`${API_BASE}/vms/${vmName}/snapshots/${encodedSnapshotName}`, {
  866. method: 'DELETE'
  867. });
  868. const data = await response.json();
  869. if (data.error) {
  870. alert(`Error: ${data.error}`);
  871. return;
  872. }
  873. alert(`✓ Snapshot "${snapshotName}" eliminado exitosamente`);
  874. // Recargar lista de snapshots
  875. loadSnapshotsList(vmName);
  876. } catch (error) {
  877. console.error('Error:', error);
  878. alert(`Error al eliminar snapshot: ${error.message}`);
  879. }
  880. }
  881. // Performance monitoring
  882. let statsInterval = null;
  883. // Initialize and update performance indicators
  884. async function initializePerformanceCharts(vmName) {
  885. // Clear any existing interval
  886. if (statsInterval) clearInterval(statsInterval);
  887. // Update immediately
  888. await updatePerformanceStats(vmName);
  889. // Update every 2 seconds
  890. statsInterval = setInterval(() => updatePerformanceStats(vmName), 2000);
  891. }
  892. // Update performance statistics
  893. async function updatePerformanceStats(vmName) {
  894. try {
  895. const response = await fetch(`${API_BASE}/vms/${vmName}/stats`);
  896. const data = await response.json();
  897. if (data.error || data.state === 'error') {
  898. console.log('VM not running or stats not available');
  899. return;
  900. }
  901. // Update CPU bar
  902. const cpuPercent = data.cpu_percent;
  903. const cpuBar = document.getElementById('cpuBar');
  904. const cpuPercentEl = document.getElementById('cpuPercent');
  905. if (cpuBar) {
  906. cpuBar.style.width = cpuPercent + '%';
  907. cpuBar.setAttribute('aria-valuenow', cpuPercent);
  908. }
  909. if (cpuPercentEl) {
  910. cpuPercentEl.textContent = cpuPercent.toFixed(1) + '%';
  911. }
  912. // Update Memory bar
  913. const memPercent = data.memory_percent;
  914. const memBar = document.getElementById('memBar');
  915. const memPercentEl = document.getElementById('memPercent');
  916. if (memBar) {
  917. memBar.style.width = memPercent + '%';
  918. memBar.setAttribute('aria-valuenow', memPercent);
  919. }
  920. if (memPercentEl) {
  921. memPercentEl.textContent = memPercent.toFixed(1) + '%';
  922. }
  923. } catch (error) {
  924. console.error('Error updating stats:', error);
  925. }
  926. }
  927. // Clean up performance monitoring when switching VMs
  928. function cleanupCharts() {
  929. if (statsInterval) {
  930. clearInterval(statsInterval);
  931. statsInterval = null;
  932. }
  933. }