File app-v2.html of Package PersistenceOS
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PersistenceOS - Dashboard (v2 Backend)</title>
<link rel="icon" href="img/favicon.ico" type="image/x-icon">
<!-- FontAwesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
/* Reset and Base Styles */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; color: #333; }
.app-container { display: flex; min-height: 100vh; }
/* Sidebar Styles */
.sidebar {
width: 250px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
transition: width 0.3s ease;
position: relative;
}
.sidebar.collapsed { width: 60px; }
.sidebar.collapsed .nav-label { display: none; }
.sidebar.collapsed .logo-container h1 { display: none; }
.sidebar.collapsed .user-details { display: none; }
.sidebar.collapsed .system-status span { display: none; }
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(255,255,255,0.1);
position: relative;
}
.logo-container {
display: flex;
align-items: center;
gap: 1rem;
}
.hamburger-menu {
cursor: pointer;
padding: 0.5rem;
border-radius: 4px;
transition: background 0.3s;
}
.hamburger-menu:hover {
background: rgba(255,255,255,0.1);
}
.logo-container h1 {
font-size: 20px;
font-weight: 300;
flex: 1;
}
.logo-icon {
font-size: 24px;
}
.sidebar-nav { flex: 1; padding: 1rem 0; }
.nav-list { list-style: none; }
.nav-item { margin: 0.25rem 0; }
.nav-item a { display: flex; align-items: center; padding: 0.75rem 1.5rem; color: rgba(255,255,255,0.8); text-decoration: none; transition: all 0.3s; }
.nav-item a:hover, .nav-item.active a { background: rgba(255,255,255,0.1); color: white; }
.nav-item i { width: 20px; margin-right: 0.75rem; }
.sidebar-footer { padding: 1rem; border-top: 1px solid rgba(255,255,255,0.1); }
.user-info { display: flex; align-items: center; margin-bottom: 1rem; }
.user-avatar { width: 40px; height: 40px; background: rgba(255,255,255,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 0.75rem; }
.user-details { flex: 1; }
.btn-link { color: rgba(255,255,255,0.8); text-decoration: none; font-size: 0.875rem; }
.btn-link:hover { color: white; }
/* Main Content */
.main-content { flex: 1; display: flex; flex-direction: column; }
.header { background: white; padding: 1.5rem 2rem; border-bottom: 1px solid #e0e6ed; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.content-container { flex: 1; padding: 2rem; }
.content-section { display: none; }
.content-section.active { display: block; }
/* Card Styles */
.card {
background: white;
border-radius: 12px;
margin-bottom: 1.5rem;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
overflow: hidden;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 1.5rem 0;
margin-bottom: 1rem;
}
.card-title {
color: #667eea;
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.card-body {
padding: 0 1.5rem 1.5rem;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 1.5rem;
}
/* System Stats */
.system-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
text-align: center;
padding: 1.5rem 1rem;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 12px;
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-2px);
}
.stat-icon {
font-size: 2rem;
color: #667eea;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 1.5rem;
font-weight: bold;
color: #333;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.75rem;
color: #666;
font-weight: 600;
letter-spacing: 0.5px;
}
/* Resource Usage */
.resource-usage {
margin-top: 2rem;
}
.usage-item {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.usage-item label {
min-width: 120px;
font-weight: 500;
color: #495057;
}
.progress-bar {
flex: 1;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 0.3s ease;
}
.usage-item span {
min-width: 50px;
text-align: right;
font-weight: 600;
color: #667eea;
}
/* Quick Actions */
.quick-actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
/* Status Items */
.status-items {
display: flex;
flex-direction: column;
gap: 1rem;
}
.status-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
background: #f8f9fa;
border-radius: 8px;
}
.status-online {
color: #28a745;
}
.status-text {
margin-left: auto;
font-weight: 500;
color: #28a745;
}
/* VM Overview Styles */
.vm-overview {
display: flex;
justify-content: space-around;
gap: 1rem;
}
.vm-stat {
text-align: center;
flex: 1;
}
.vm-count {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.vm-count.running {
color: #28a745;
}
.vm-count.stopped {
color: #6c757d;
}
.vm-count.total {
color: #667eea;
}
.vm-label {
font-size: 0.875rem;
color: #666;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Table Styles */
.table-container {
overflow-x: auto;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.table {
width: 100%;
border-collapse: collapse;
margin: 0;
}
.table th,
.table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #e9ecef;
}
.table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.table tbody tr:hover {
background: #f8f9fa;
}
/* Button Group */
.btn-group {
display: flex;
gap: 0.25rem;
}
.btn-sm {
padding: 0.375rem 0.5rem;
font-size: 0.75rem;
}
/* Status Badges */
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-running {
background: #d4edda;
color: #155724;
}
.status-stopped {
background: #f8d7da;
color: #721c24;
}
.status-paused {
background: #fff3cd;
color: #856404;
}
/* Storage Pool Card Styles */
.storage-pool-card {
min-height: 200px;
display: flex;
flex-direction: column;
}
.pool-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.pool-header h4 {
margin: 0;
color: #667eea;
}
.pool-type {
background: #e9ecef;
color: #495057;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
}
.pool-usage {
flex: 1;
margin-bottom: 1rem;
}
.usage-bar {
width: 100%;
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.usage-fill {
height: 100%;
transition: width 0.3s ease;
}
.usage-fill.low {
background: linear-gradient(90deg, #28a745, #20c997);
}
.usage-fill.medium {
background: linear-gradient(90deg, #ffc107, #fd7e14);
}
.usage-fill.high {
background: linear-gradient(90deg, #dc3545, #e83e8c);
}
.pool-status {
display: flex;
justify-content: space-between;
align-items: center;
}
.pool-actions {
display: flex;
gap: 0.25rem;
}
/* Type Badge */
.type-badge {
background: #667eea;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
/* Snapshot Timeline Styles */
.snapshot-timeline {
position: relative;
padding-left: 2rem;
}
.snapshot-timeline::before {
content: '';
position: absolute;
left: 1rem;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(to bottom, #667eea, #764ba2);
}
.snapshot-timeline-item {
position: relative;
margin-bottom: 2rem;
}
.snapshot-timeline-marker {
position: absolute;
left: -2rem;
top: 0.5rem;
width: 2rem;
height: 2rem;
background: #667eea;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
z-index: 1;
}
.snapshot-timeline-content {
margin-left: 1rem;
}
.snapshot-card {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.snapshot-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.snapshot-info h4 {
margin: 0 0 0.5rem 0;
color: #333;
}
.snapshot-meta {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: #6c757d;
}
.snapshot-type {
background: #e9ecef;
color: #495057;
padding: 0.125rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.snapshot-size {
font-weight: 600;
color: #667eea;
}
.snapshot-details {
margin-bottom: 1rem;
}
.snapshot-description {
color: #495057;
margin-bottom: 0.5rem;
}
.snapshot-path {
font-size: 0.875rem;
color: #6c757d;
}
.snapshot-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
/* Retention Policy Cards */
.retention-policy-card {
min-height: 150px;
}
.retention-policy-card h4 {
color: #667eea;
margin-bottom: 1rem;
}
.policy-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.policy-details div {
font-size: 0.875rem;
color: #495057;
}
/* Network Styles */
.dns-servers {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.dns-server-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: #f8f9fa;
border-radius: 6px;
}
.dns-status {
margin-left: auto;
font-size: 0.75rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.dns-status.online {
background: #d4edda;
color: #155724;
}
.dns-status.offline {
background: #f8d7da;
color: #721c24;
}
.firewall-info,
.routing-info {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.firewall-stat,
.route-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
}
.firewall-stat:last-child,
.route-item:last-child {
border-bottom: none;
}
.text-success {
color: #28a745 !important;
}
.text-danger {
color: #dc3545 !important;
}
/* Status indicators */
.status-offline {
color: #6c757d;
}
.status-warning {
color: #ffc107;
}
/* Settings Styles */
.system-info-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.75rem;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
}
.info-item:last-child {
border-bottom: none;
}
.health-indicators {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.health-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: #f8f9fa;
border-radius: 6px;
}
.health-status {
margin-left: auto;
font-weight: 500;
}
.settings-card {
text-align: center;
padding: 2rem 1.5rem;
min-height: 200px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.settings-icon {
font-size: 3rem;
color: #667eea;
margin-bottom: 1rem;
}
.settings-card h4 {
color: #333;
margin-bottom: 0.5rem;
}
.settings-card p {
color: #6c757d;
font-size: 0.875rem;
margin-bottom: 1.5rem;
flex: 1;
}
/* Tables */
.table-container { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #e0e6ed; }
.table th { background: #f8f9fa; font-weight: 600; color: #495057; }
.table tbody tr:hover { background: #f8f9fa; }
/* Status badges */
.status-badge { padding: 0.25rem 0.5rem; border-radius: 4px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
.status-running { background: #d4edda; color: #155724; }
.status-stopped { background: #f8d7da; color: #721c24; }
.status-healthy { background: #d4edda; color: #155724; }
.status-warning { background: #fff3cd; color: #856404; }
.status-error { background: #f8d7da; color: #721c24; }
/* Buttons */
.btn { padding: 0.5rem 1rem; border: none; border-radius: 6px; cursor: pointer; font-size: 0.875rem; font-weight: 500; text-decoration: none; display: inline-flex; align-items: center; gap: 0.5rem; transition: all 0.3s; }
.btn-primary { background: #0066cc; color: white; }
.btn-primary:hover { background: #0052a3; }
.btn-success { background: #28a745; color: white; }
.btn-success:hover { background: #218838; }
.btn-danger { background: #dc3545; color: white; }
.btn-danger:hover { background: #c82333; }
.btn-secondary { background: #6c757d; color: white; }
.btn-secondary:hover { background: #5a6268; }
/* Forms */
.form-group { margin-bottom: 1rem; }
.form-label { display: block; margin-bottom: 0.5rem; font-weight: 500; color: #495057; }
.form-control { width: 100%; padding: 0.75rem; border: 1px solid #ced4da; border-radius: 6px; font-size: 1rem; }
.form-control:focus { outline: none; border-color: #0066cc; box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1); }
/* Modals */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal { background: white; border-radius: 12px; padding: 2rem; max-width: 500px; width: 90%; max-height: 90vh; overflow-y: auto; }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.modal-title { font-size: 1.25rem; font-weight: 600; color: #0066cc; }
.modal-close { background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #6c757d; }
.modal-close:hover { color: #495057; }
/* Notifications */
.notification { position: fixed; top: 20px; right: 20px; padding: 1rem 1.5rem; border-radius: 8px; color: white; font-weight: 500; z-index: 1001; animation: slideIn 0.3s ease; }
.notification.success { background: #28a745; }
.notification.error { background: #dc3545; }
.notification.warning { background: #ffc107; color: #212529; }
.notification.info { background: #17a2b8; }
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* Responsive */
@media (max-width: 768px) {
.sidebar { width: 200px; }
.system-stats { grid-template-columns: 1fr; }
.dashboard-grid { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.sidebar { position: fixed; left: -250px; z-index: 999; transition: left 0.3s; }
.sidebar.open { left: 0; }
.main-content { margin-left: 0; }
.header { padding: 1rem; }
.content-container { padding: 1rem; }
}
/* Loading states */
.loading { opacity: 0.6; pointer-events: none; }
.spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid #f3f3f3; border-top: 2px solid #0066cc; border-radius: 50%; animation: spin 1s linear infinite; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
/* Network interface styles */
.interface-card { border-left: 4px solid #0066cc; }
.interface-status { display: flex; align-items: center; gap: 0.5rem; }
.status-dot { width: 8px; height: 8px; border-radius: 50%; }
.status-dot.up { background: #28a745; }
.status-dot.down { background: #dc3545; }
/* VM status styles */
.vm-card { transition: all 0.3s ease; }
.vm-card:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,0,0,0.15); }
.vm-resources { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-top: 1rem; }
.resource-item { text-align: center; padding: 0.5rem; background: #f8f9fa; border-radius: 6px; }
/* Storage pool styles */
.pool-usage { margin: 1rem 0; }
.usage-bar { background: #e9ecef; height: 6px; border-radius: 3px; overflow: hidden; }
.usage-fill { height: 100%; transition: width 0.3s ease; }
.usage-fill.low { background: #28a745; }
.usage-fill.medium { background: #ffc107; }
.usage-fill.high { background: #dc3545; }
/* Snapshot styles */
.snapshot-item { display: flex; justify-content: space-between; align-items: center; padding: 1rem; border: 1px solid #e0e6ed; border-radius: 8px; margin-bottom: 0.5rem; }
.snapshot-info h4 { color: #0066cc; margin-bottom: 0.25rem; }
.snapshot-meta { font-size: 0.875rem; color: #6c757d; }
.snapshot-actions { display: flex; gap: 0.5rem; }
/* Dashboard specific */
.metric-card { text-align: center; }
.metric-value { font-size: 2.5rem; font-weight: bold; color: #0066cc; }
.metric-change { font-size: 0.875rem; margin-top: 0.5rem; }
.metric-change.positive { color: #28a745; }
.metric-change.negative { color: #dc3545; }
/* Chart placeholder */
.chart-placeholder { height: 200px; background: #f8f9fa; border-radius: 8px; display: flex; align-items: center; justify-content: center; color: #6c757d; }
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
/* Prevent body scrolling when modal is open */
body.modal-open {
overflow: hidden !important;
}
.modal-content {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: translateY(-50px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.modal-header {
padding: 1.5rem 1.5rem 1rem;
border-bottom: 1px solid #e9ecef;
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-title {
font-size: 1.25rem;
font-weight: 600;
color: #333;
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
color: #6c757d;
cursor: pointer;
padding: 0.25rem;
border-radius: 4px;
transition: all 0.2s;
}
/* Enhanced styling for VM creation form */
.form-section {
margin-bottom: 2rem;
padding: 1.5rem;
border: 1px solid #e9ecef;
border-radius: 8px;
background: #f8f9fa;
}
.form-section h4 {
color: #495057;
margin-bottom: 1rem;
font-size: 1.1rem;
font-weight: 600;
}
.form-section h4 i {
margin-right: 0.5rem;
color: #007bff;
}
/* Enhanced input styling for VM creation */
.modal-content input[type="number"] {
font-weight: 500;
text-align: center;
transition: all 0.2s ease;
}
.modal-content input[type="number"]:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
transform: scale(1.02);
}
.modal-content .form-text {
color: #6c757d;
font-size: 0.875rem;
margin-top: 0.25rem;
font-style: italic;
}
.form-row {
display: flex;
gap: 1rem;
}
.form-row .form-group {
flex: 1;
}
/* VM Console Modal Styling */
.vm-console-modal {
background: #1a1a1a;
color: #ffffff;
}
.vm-console-modal .modal-header {
background: #2d2d2d;
border-bottom: 1px solid #444;
padding: 1rem 1.5rem;
}
.vm-console-modal .modal-header h3 {
color: #ffffff;
margin: 0;
font-size: 1.1rem;
}
.console-controls {
display: flex;
gap: 0.5rem;
align-items: center;
}
.console-body {
padding: 0;
background: #000000;
min-height: 500px;
}
.vm-console-container {
display: flex;
flex-direction: column;
height: 100%;
min-height: 500px;
}
.console-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
color: #00ff00;
font-family: monospace;
}
.console-loading i {
font-size: 2rem;
margin-bottom: 1rem;
}
.console-preview {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
background: #000000;
padding: 1rem;
}
#vncCanvas {
border: 2px solid #00ff00;
background: #000000;
cursor: crosshair;
max-width: 100%;
max-height: 100%;
}
.console-info {
background: #2d2d2d;
padding: 1rem;
border-top: 1px solid #444;
}
.console-info .info-item {
margin-bottom: 0.5rem;
font-family: monospace;
font-size: 0.9rem;
}
.console-footer {
background: #2d2d2d;
border-top: 1px solid #444;
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
}
.console-stats {
display: flex;
gap: 2rem;
font-family: monospace;
font-size: 0.85rem;
color: #cccccc;
}
.console-actions {
display: flex;
gap: 0.5rem;
}
.console-unavailable,
.console-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
text-align: center;
color: #ffffff;
padding: 2rem;
}
.console-unavailable i,
.console-error i {
font-size: 3rem;
margin-bottom: 1rem;
color: #ffc107;
}
.console-error i {
color: #dc3545;
}
.console-unavailable h4,
.console-error h4 {
color: #ffffff;
margin-bottom: 1rem;
}
.console-unavailable ul {
text-align: left;
margin: 1rem 0;
}
.error-message {
color: #dc3545;
font-family: monospace;
background: #2d2d2d;
padding: 0.5rem;
border-radius: 4px;
margin: 1rem 0;
}
.modal-close:hover {
background: #f8f9fa;
color: #333;
}
.modal-body {
padding: 1.5rem;
}
.modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid #e9ecef;
display: flex;
gap: 0.75rem;
justify-content: flex-end;
}
/* Form Styles for Modals */
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #495057;
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 6px;
font-size: 0.875rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-control::placeholder {
color: #6c757d;
}
/* Button Styles */
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
text-align: center;
justify-content: center;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-1px);
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
transform: translateY(-1px);
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
transform: translateY(-1px);
}
</style>
</head>
<body>
<div id="app" class="app-container">
<!-- Sidebar -->
<div class="sidebar" :class="{ collapsed: sidebarCollapsed }">
<div class="sidebar-header">
<div class="logo-container">
<div class="hamburger-menu" @click="toggleSidebar">
<i class="fas fa-bars"></i>
</div>
<h1>PersistenceOS</h1>
<div class="logo-icon">
<i class="fas fa-server"></i>
</div>
</div>
</div>
<nav class="sidebar-nav">
<ul class="nav-list">
<li class="nav-item" :class="{ active: activeSection === 'dashboard' }">
<a href="#" @click="setActiveSection('dashboard')">
<i class="fas fa-tachometer-alt"></i>
<span class="nav-label">Overview</span>
</a>
</li>
<li class="nav-item" :class="{ active: activeSection === 'virtualization' }">
<a href="#" @click="setActiveSection('virtualization')">
<i class="fas fa-desktop"></i>
<span class="nav-label">Virtualization</span>
</a>
</li>
<li class="nav-item" :class="{ active: activeSection === 'storage' }">
<a href="#" @click="setActiveSection('storage')">
<i class="fas fa-hdd"></i>
<span class="nav-label">Storage</span>
</a>
</li>
<li class="nav-item" :class="{ active: activeSection === 'snapshots' }">
<a href="#" @click="setActiveSection('snapshots')">
<i class="fas fa-camera"></i>
<span class="nav-label">Snapshots</span>
</a>
</li>
<li class="nav-item" :class="{ active: activeSection === 'network' }">
<a href="#" @click="setActiveSection('network')">
<i class="fas fa-network-wired"></i>
<span class="nav-label">Network</span>
</a>
</li>
<li class="nav-item" :class="{ active: activeSection === 'settings' }">
<a href="#" @click="setActiveSection('settings')">
<i class="fas fa-cog"></i>
<span class="nav-label">Settings</span>
</a>
</li>
</ul>
</nav>
<div class="sidebar-footer">
<div class="user-info">
<div class="user-avatar">
<span class="avatar-text">R</span>
</div>
<div class="user-details">
<span>root</span>
<a href="#" class="btn-link" @click="logout">
<i class="fas fa-sign-out-alt"></i> Logout
</a>
</div>
</div>
<div class="system-status">
<div class="status-indicator healthy"></div>
<span>System Healthy</span>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<div class="header">
<h2 class="bold-heading">{{ getSectionTitle }}</h2>
</div>
<div class="content-container">
<!-- Dashboard Section -->
<div class="content-section" :class="{ active: activeSection === 'dashboard' }">
<div class="dashboard-grid">
<!-- System Information Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">System Information</h3>
<i class="fas fa-info-circle"></i>
</div>
<div class="card-body">
<div class="system-stats">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-server"></i>
</div>
<div class="stat-value">{{ systemStats.hostname || 'localhost.localdomain' }}</div>
<div class="stat-label">HOSTNAME</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-code-branch"></i>
</div>
<div class="stat-value">{{ systemStats.version || 'PersistenceOS 6.1.0' }}</div>
<div class="stat-label">VERSION</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-clock"></i>
</div>
<div class="stat-value">{{ formatUptime(systemStats.uptime) || 'Online' }}</div>
<div class="stat-label">STATUS</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-history"></i>
</div>
<div class="stat-value">{{ formatUptime(systemStats.uptime) || '7 hours, 42 minutes' }}</div>
<div class="stat-label">UPTIME</div>
</div>
</div>
<!-- Data Source Indicator -->
<div class="data-source-indicator" v-if="systemStats.source" style="margin: 1rem 0; padding: 0.5rem; background: rgba(255,255,255,0.1); border-radius: 5px; font-size: 0.875rem;">
<i class="fas fa-check-circle" style="color: #28a745;"></i>
<span style="margin-left: 0.5rem;">
Data Source: Real System ({{ systemStats.source || 'API' }})
</span>
</div>
<!-- Resource Usage -->
<div class="resource-usage">
<div class="usage-item">
<label>CPU Usage</label>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: (systemStats.cpu?.usage || systemStats.cpu || 0) + '%' }"></div>
</div>
<span>{{ (systemStats.cpu?.usage || systemStats.cpu || 0) }}%</span>
</div>
<div class="usage-item">
<label>Memory Usage</label>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: (systemStats.memory?.percent || systemStats.memory || 19.7) + '%' }"></div>
</div>
<span>{{ (systemStats.memory?.percent || systemStats.memory || 19.7) }}%</span>
</div>
<div class="usage-item">
<label>Storage Usage</label>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: (systemStats.disk?.percent || systemStats.disk || 3.3) + '%' }"></div>
</div>
<span>{{ (systemStats.disk?.percent || systemStats.disk || 3.3) }}%</span>
</div>
</div>
</div>
</div>
<!-- Quick Actions Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Quick Actions</h3>
</div>
<div class="card-body">
<div class="quick-actions">
<button class="btn btn-primary" @click="setActiveSection('virtualization')">
<i class="fas fa-desktop"></i> Manage Virtual Machines
</button>
<button class="btn btn-success" @click="setActiveSection('storage')">
<i class="fas fa-hdd"></i> Storage Management
</button>
<button class="btn btn-info" @click="setActiveSection('snapshots')">
<i class="fas fa-camera"></i> Snapshot Management
</button>
<button class="btn btn-warning" @click="setActiveSection('network')">
<i class="fas fa-network-wired"></i> Network Configuration
</button>
<button class="btn btn-secondary" @click="setActiveSection('settings')">
<i class="fas fa-cog"></i> System Settings
</button>
</div>
</div>
</div>
<!-- Status Overview Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Status Overview</h3>
</div>
<div class="card-body">
<div class="status-items">
<div class="status-item">
<i class="fas fa-circle status-online"></i>
<span>API Service</span>
<span class="status-text">Online</span>
</div>
<div class="status-item">
<i class="fas fa-circle status-online"></i>
<span>Storage Pool</span>
<span class="status-text">Healthy</span>
</div>
<div class="status-item">
<i class="fas fa-circle status-online"></i>
<span>Network</span>
<span class="status-text">Connected</span>
</div>
<div class="status-item">
<i class="fas fa-circle status-online"></i>
<span>Snapshots</span>
<span class="status-text">Active</span>
</div>
</div>
</div>
</div>
</div>
<!-- Virtual Machines Overview -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Virtual Machines</h3>
<div class="data-source-indicator">
<i class="fas fa-check-circle" style="color: #28a745;"></i>
<span class="data-source-text">Real System ({{ vmDataSource || 'libvirt' }})</span>
</div>
<i class="fas fa-expand-arrows-alt"></i>
</div>
<div class="card-body">
<div class="vm-overview">
<div class="vm-stat">
<div class="vm-count running">{{ vmStats.running || 0 }}</div>
<div class="vm-label">Running</div>
</div>
<div class="vm-stat">
<div class="vm-count stopped">{{ vmStats.stopped || 0 }}</div>
<div class="vm-label">Stopped</div>
</div>
<div class="vm-stat">
<div class="vm-count total">{{ vmStats.total || 0 }}</div>
<div class="vm-label">Total</div>
</div>
</div>
</div>
</div>
</div>
<!-- Virtualization Section -->
<div class="content-section" :class="{ active: activeSection === 'virtualization' }">
<!-- VM Statistics Overview -->
<div class="dashboard-grid" style="margin-bottom: 2rem;">
<div class="card">
<div class="card-header">
<h3 class="card-title">VM Statistics</h3>
<i class="fas fa-chart-bar"></i>
</div>
<div class="card-body">
<div class="system-stats">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-play"></i>
</div>
<div class="stat-value">{{ vmStats.running || 0 }}</div>
<div class="stat-label">RUNNING</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-stop"></i>
</div>
<div class="stat-value">{{ vmStats.stopped || 0 }}</div>
<div class="stat-label">STOPPED</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-desktop"></i>
</div>
<div class="stat-value">{{ vmStats.total || 0 }}</div>
<div class="stat-label">TOTAL VMs</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Resource Usage</h3>
<i class="fas fa-tachometer-alt"></i>
</div>
<div class="card-body">
<div class="resource-usage">
<div class="usage-item">
<label>CPU Allocation</label>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: (vmStats.cpuUsage || 0) + '%' }"></div>
</div>
<span>{{ (vmStats.cpuUsage || 0) }}%</span>
</div>
<div class="usage-item">
<label>Memory Allocation</label>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: (vmStats.memoryUsage || 0) + '%' }"></div>
</div>
<span>{{ (vmStats.memoryUsage || 0) }}%</span>
</div>
<div class="usage-item">
<label>Storage Usage</label>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: (vmStats.storageUsage || 0) + '%' }"></div>
</div>
<span>{{ (vmStats.storageUsage || 0) }}%</span>
</div>
</div>
</div>
</div>
</div>
<!-- VM Management -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Virtual Machine Management</h3>
<div>
<button class="btn btn-primary" @click="showCreateVMModal()" style="margin-right: 0.5rem;">
<i class="fas fa-plus"></i> Create VM
</button>
<button class="btn btn-success" @click="refreshVMData">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
</div>
<div class="card-body">
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>CPU Cores</th>
<th>Memory</th>
<th>Storage</th>
<th>Uptime</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="vm in vms" :key="vm.id">
<td>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-desktop" style="color: #667eea;"></i>
<strong>{{ vm.name }}</strong>
</div>
</td>
<td>
<span class="status-badge" :class="'status-' + vm.status">
<i class="fas" :class="vm.status === 'running' ? 'fa-play' : 'fa-stop'"></i>
{{ vm.status }}
</span>
</td>
<td>{{ vm.cpu || vm.vcpus || 'N/A' }}</td>
<td>{{ formatBytes(vm.memory) || 'N/A' }}</td>
<td>{{ formatBytes(vm.storage) || 'N/A' }}</td>
<td>{{ vm.uptime || 'N/A' }}</td>
<td>
<div class="btn-group">
<button v-if="vm.status === 'stopped'"
class="btn btn-success btn-sm"
@click="startVM(vm.id)"
title="Start VM">
<i class="fas fa-play"></i>
</button>
<button v-if="vm.status === 'running'"
class="btn btn-warning btn-sm"
@click="stopVM(vm.id)"
title="Stop VM">
<i class="fas fa-stop"></i>
</button>
<button v-if="vm.status === 'running'"
class="btn btn-danger btn-sm"
@click="rebootVM(vm.id)"
title="Reboot VM">
<i class="fas fa-redo"></i>
</button>
<button class="btn btn-info btn-sm"
@click="showVMConsole(vm.id, vm.name)"
title="VM Console">
<i class="fas fa-desktop"></i>
</button>
<button class="btn btn-info btn-sm"
@click="editVM(vm.id)"
title="Edit VM">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-secondary btn-sm"
@click="deleteVM(vm.id)"
title="Delete VM">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
<tr v-if="vms.length === 0">
<td colspan="7" style="text-align: center; padding: 2rem; color: #6c757d;">
<i class="fas fa-desktop" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;"></i>
<div>No virtual machines found</div>
<div style="font-size: 0.875rem; margin-top: 0.5rem;">Click "Create VM" to get started</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Settings Section -->
<div class="content-section" :class="{ active: activeSection === 'settings' }">
<!-- System Information -->
<div class="dashboard-grid" style="margin-bottom: 2rem;">
<div class="card">
<div class="card-header">
<h3 class="card-title">System Information</h3>
<i class="fas fa-info-circle"></i>
</div>
<div class="card-body">
<div class="system-info-grid">
<div class="info-item">
<strong>Hostname:</strong>
<span>{{ systemStats.hostname || 'localhost.localdomain' }}</span>
</div>
<div class="info-item">
<strong>OS Version:</strong>
<span>{{ systemStats.version || 'PersistenceOS 6.1.0' }}</span>
</div>
<div class="info-item">
<strong>Kernel:</strong>
<span>Linux 6.1.0-persistence</span>
</div>
<div class="info-item">
<strong>Architecture:</strong>
<span>x86_64</span>
</div>
<div class="info-item">
<strong>Boot Time:</strong>
<span>{{ getBootTime() }}</span>
</div>
<div class="info-item">
<strong>Last Update:</strong>
<span>{{ getLastUpdateTime() }}</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">System Health</h3>
<i class="fas fa-heartbeat"></i>
</div>
<div class="card-body">
<div class="health-indicators">
<div class="health-item">
<i class="fas fa-circle status-online"></i>
<span>System Services</span>
<span class="health-status">Healthy</span>
</div>
<div class="health-item">
<i class="fas fa-circle status-online"></i>
<span>Storage Health</span>
<span class="health-status">Good</span>
</div>
<div class="health-item">
<i class="fas fa-circle status-online"></i>
<span>Network Status</span>
<span class="health-status">Connected</span>
</div>
<div class="health-item">
<i class="fas fa-circle status-warning"></i>
<span>Security Updates</span>
<span class="health-status">Available</span>
</div>
</div>
</div>
</div>
</div>
<!-- System Configuration -->
<div class="card">
<div class="card-header">
<h3 class="card-title">System Configuration</h3>
</div>
<div class="card-body">
<div class="dashboard-grid">
<div class="card settings-card">
<div class="settings-icon">
<i class="fas fa-server"></i>
</div>
<h4>General Settings</h4>
<p>Configure hostname, timezone, and basic system settings</p>
<button class="btn btn-primary" @click="showGeneralSettings">
<i class="fas fa-cog"></i> Configure
</button>
</div>
<div class="card settings-card">
<div class="settings-icon">
<i class="fas fa-users"></i>
</div>
<h4>User Management</h4>
<p>Manage user accounts, permissions, and authentication</p>
<button class="btn btn-primary" @click="showUserManagement">
<i class="fas fa-users"></i> Manage Users
</button>
</div>
<div class="card settings-card">
<div class="settings-icon">
<i class="fas fa-shield-alt"></i>
</div>
<h4>Security Settings</h4>
<p>Configure firewall, SSH, and security policies</p>
<button class="btn btn-primary" @click="showSecuritySettings">
<i class="fas fa-shield-alt"></i> Security
</button>
</div>
<div class="card settings-card">
<div class="settings-icon">
<i class="fas fa-download"></i>
</div>
<h4>System Updates</h4>
<p>Check for and install system updates using transactional-update</p>
<button class="btn btn-success" @click="checkSystemUpdates">
<i class="fas fa-download"></i> Check Updates
</button>
</div>
<div class="card settings-card">
<div class="settings-icon">
<i class="fas fa-clock"></i>
</div>
<h4>Date & Time</h4>
<p>Configure system time, timezone, and NTP settings</p>
<button class="btn btn-primary" @click="showDateTimeSettings">
<i class="fas fa-clock"></i> Configure
</button>
</div>
<div class="card settings-card">
<div class="settings-icon">
<i class="fas fa-chart-line"></i>
</div>
<h4>Monitoring</h4>
<p>Configure system monitoring, alerts, and logging</p>
<button class="btn btn-primary" @click="showMonitoringSettings">
<i class="fas fa-chart-line"></i> Configure
</button>
</div>
</div>
</div>
</div>
<!-- Advanced Settings -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Advanced Settings</h3>
</div>
<div class="card-body">
<div class="dashboard-grid">
<div class="card settings-card">
<div class="settings-icon">
<i class="fas fa-terminal"></i>
</div>
<h4>System Services</h4>
<p>Manage systemd services and system processes</p>
<button class="btn btn-warning" @click="showSystemServices">
<i class="fas fa-list"></i> View Services
</button>
</div>
<div class="card settings-card">
<div class="settings-icon">
<i class="fas fa-file-alt"></i>
</div>
<h4>System Logs</h4>
<p>View and manage system logs and journal entries</p>
<button class="btn btn-info" @click="showSystemLogs">
<i class="fas fa-file-alt"></i> View Logs
</button>
</div>
<div class="card settings-card">
<div class="settings-icon">
<i class="fas fa-database"></i>
</div>
<h4>Backup & Restore</h4>
<p>Configure system backups and restore points</p>
<button class="btn btn-primary" @click="showBackupSettings">
<i class="fas fa-database"></i> Configure
</button>
</div>
<div class="card settings-card">
<div class="settings-icon">
<i class="fas fa-power-off"></i>
</div>
<h4>Power Management</h4>
<p>System shutdown, restart, and power options</p>
<div class="btn-group" style="width: 100%;">
<button class="btn btn-warning" @click="restartSystem">
<i class="fas fa-redo"></i> Restart
</button>
<button class="btn btn-danger" @click="shutdownSystem">
<i class="fas fa-power-off"></i> Shutdown
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- VMs Section (Legacy) -->
<div class="content-section" :class="{ active: activeSection === 'vms' }">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3><i class="fas fa-desktop"></i> Virtual Machines</h3>
<button class="btn btn-primary" @click="showCreateVMModal()">
<i class="fas fa-plus"></i> Create VM
</button>
</div>
<div class="system-stats" style="margin-bottom: 2rem;">
<div class="stat-item">
<div class="stat-value">{{ vmStats.running }}</div>
<div class="stat-label">Running</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ vmStats.stopped }}</div>
<div class="stat-label">Stopped</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ vmStats.total }}</div>
<div class="stat-label">Total VMs</div>
</div>
</div>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th>CPU</th>
<th>Memory</th>
<th>Uptime</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="vm in vms" :key="vm.id">
<td><strong>{{ vm.name }}</strong></td>
<td>
<span class="status-badge" :class="'status-' + vm.status">
{{ vm.status }}
</span>
</td>
<td>{{ vm.cpu }}</td>
<td>{{ vm.memory }}</td>
<td>{{ vm.uptime || 'N/A' }}</td>
<td>
<button v-if="vm.status === 'stopped'"
class="btn btn-success"
@click="startVM(vm.id)"
style="margin-right: 0.5rem;">
<i class="fas fa-play"></i>
</button>
<button v-if="vm.status === 'running'"
class="btn btn-danger"
@click="stopVM(vm.id)"
style="margin-right: 0.5rem;">
<i class="fas fa-stop"></i>
</button>
<button class="btn btn-secondary" @click="deleteVM(vm.id)">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Storage Section -->
<div class="content-section" :class="{ active: activeSection === 'storage' }">
<!-- Storage Overview -->
<div class="dashboard-grid" style="margin-bottom: 2rem;">
<div class="card">
<div class="card-header">
<h3 class="card-title">Storage Overview</h3>
<i class="fas fa-chart-pie"></i>
</div>
<div class="card-body">
<div class="system-stats">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-hdd"></i>
</div>
<div class="stat-value">{{ storagePools.length || 0 }}</div>
<div class="stat-label">POOLS</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-database"></i>
</div>
<div class="stat-value">{{ datasets.length || 0 }}</div>
<div class="stat-label">DATASETS</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-chart-area"></i>
</div>
<div class="stat-value">{{ formatBytes(getTotalStorageUsed()) }}</div>
<div class="stat-label">USED</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Filesystem Health</h3>
<i class="fas fa-heartbeat"></i>
</div>
<div class="card-body">
<div class="status-items">
<div class="status-item">
<i class="fas fa-circle status-online"></i>
<span>BTRFS System</span>
<span class="status-text">Healthy</span>
</div>
<div class="status-item">
<i class="fas fa-circle status-online"></i>
<span>XFS Storage</span>
<span class="status-text">Healthy</span>
</div>
<div class="status-item">
<i class="fas fa-circle status-online"></i>
<span>LVM Volumes</span>
<span class="status-text">Active</span>
</div>
</div>
</div>
</div>
</div>
<!-- Data Source Indicator -->
<div class="data-source-indicator" v-if="storagePools.length > 0" style="margin: 1rem 0; padding: 0.5rem; background: rgba(255,255,255,0.1); border-radius: 5px; font-size: 0.875rem;">
<i class="fas fa-check-circle" style="color: #28a745;"></i>
<span style="margin-left: 0.5rem;">
Storage Data: Real System (btrfs + xfs + lvm2)
</span>
</div>
<!-- Storage Pools -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Storage Pools</h3>
<div>
<button class="btn btn-primary" @click="createPool" style="margin-right: 0.5rem;">
<i class="fas fa-plus"></i> Create Pool
</button>
<button class="btn btn-success" @click="refreshStorageData">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
</div>
<div class="card-body">
<div class="dashboard-grid">
<div v-for="pool in storagePools" :key="pool.id" class="card storage-pool-card">
<div class="pool-header">
<h4>{{ pool.name }}</h4>
<span class="pool-type">{{ pool.type || 'BTRFS' }}</span>
</div>
<div class="pool-usage">
<div style="display: flex; justify-content: space-between; margin-bottom: 0.5rem;">
<span>{{ formatBytes(pool.used || 0) }} / {{ formatBytes(pool.total || 0) }}</span>
<span>{{ Math.round((pool.used || 0) / (pool.total || 1) * 100) }}%</span>
</div>
<div class="usage-bar">
<div class="usage-fill"
:class="getUsageClass(Math.round((pool.used || 0) / (pool.total || 1) * 100))"
:style="{ width: Math.round((pool.used || 0) / (pool.total || 1) * 100) + '%' }"></div>
</div>
</div>
<div class="pool-status">
<span class="status-badge" :class="'status-' + (pool.status || 'healthy')">
<i class="fas" :class="pool.status === 'healthy' ? 'fa-check' : 'fa-exclamation'"></i>
{{ pool.status || 'healthy' }}
</span>
<div class="pool-actions">
<button class="btn btn-info btn-sm" @click="managePool(pool.id)" title="Manage Pool">
<i class="fas fa-cog"></i>
</button>
<button class="btn btn-warning btn-sm" @click="scrubPool(pool.id)" title="Scrub Pool">
<i class="fas fa-broom"></i>
</button>
</div>
</div>
</div>
<div v-if="storagePools.length === 0" class="card storage-pool-card">
<div style="text-align: center; padding: 2rem; color: #6c757d;">
<i class="fas fa-hdd" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;"></i>
<div>No storage pools found</div>
<div style="font-size: 0.875rem; margin-top: 0.5rem;">Click "Create Pool" to get started</div>
</div>
</div>
</div>
</div>
</div>
<!-- Datasets -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Datasets & Volumes</h3>
<button class="btn btn-primary" @click="showCreateDatasetModal()">
<i class="fas fa-plus"></i> Create Dataset
</button>
</div>
<div class="card-body">
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Pool</th>
<th>Type</th>
<th>Used</th>
<th>Available</th>
<th>Mountpoint</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="dataset in datasets" :key="dataset.id">
<td>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<i class="fas fa-database" style="color: #667eea;"></i>
<strong>{{ dataset.name }}</strong>
</div>
</td>
<td>{{ dataset.pool }}</td>
<td>
<span class="type-badge">{{ dataset.type || 'filesystem' }}</span>
</td>
<td>{{ formatBytes(dataset.used || 0) }}</td>
<td>{{ formatBytes(dataset.available || 0) }}</td>
<td>{{ dataset.mountpoint || 'N/A' }}</td>
<td>
<div class="btn-group">
<button class="btn btn-info btn-sm" @click="editDataset(dataset.id)" title="Edit Dataset">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-success btn-sm" @click="snapshotDataset(dataset.id)" title="Create Snapshot">
<i class="fas fa-camera"></i>
</button>
<button class="btn btn-secondary btn-sm" @click="deleteDataset(dataset.id)" title="Delete Dataset">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
<tr v-if="datasets.length === 0">
<td colspan="7" style="text-align: center; padding: 2rem; color: #6c757d;">
<i class="fas fa-database" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;"></i>
<div>No datasets found</div>
<div style="font-size: 0.875rem; margin-top: 0.5rem;">Click "Create Dataset" to get started</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Network Section -->
<div class="content-section" :class="{ active: activeSection === 'network' }">
<!-- Network Overview -->
<div class="dashboard-grid" style="margin-bottom: 2rem;">
<div class="card">
<div class="card-header">
<h3 class="card-title">Network Status</h3>
<i class="fas fa-network-wired"></i>
</div>
<div class="card-body">
<div class="system-stats">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-ethernet"></i>
</div>
<div class="stat-value">{{ getActiveInterfaces() }}</div>
<div class="stat-label">ACTIVE</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-wifi"></i>
</div>
<div class="stat-value">{{ networkInterfaces.length || 0 }}</div>
<div class="stat-label">TOTAL</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-exchange-alt"></i>
</div>
<div class="stat-value">{{ formatBytes(getTotalNetworkTraffic()) }}</div>
<div class="stat-label">TRAFFIC</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Security Status</h3>
<i class="fas fa-shield-alt"></i>
</div>
<div class="card-body">
<div class="status-items">
<div class="status-item">
<i class="fas fa-circle" :class="firewallStatus.enabled ? 'status-online' : 'status-offline'"></i>
<span>Firewall (nftables)</span>
<span class="status-text" :class="firewallStatus.enabled ? 'text-success' : 'text-danger'">
{{ firewallStatus.enabled ? 'Active' : 'Inactive' }}
</span>
</div>
<div class="status-item">
<i class="fas fa-circle status-online"></i>
<span>SSH Service</span>
<span class="status-text">Active</span>
</div>
<div class="status-item">
<i class="fas fa-circle status-online"></i>
<span>Network Manager</span>
<span class="status-text">Running</span>
</div>
</div>
</div>
</div>
</div>
<!-- Data Source Indicator -->
<div class="data-source-indicator" v-if="networkInterfaces.length > 0" style="margin: 1rem 0; padding: 0.5rem; background: rgba(255,255,255,0.1); border-radius: 5px; font-size: 0.875rem;">
<i class="fas fa-check-circle" style="color: #28a745;"></i>
<span style="margin-left: 0.5rem;">
Network Data: Real System (iproute2 + NetworkManager)
</span>
</div>
<!-- Network Interfaces -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Network Interfaces</h3>
<div>
<button class="btn btn-primary" @click="showAddInterfaceModal = true" style="margin-right: 0.5rem;">
<i class="fas fa-plus"></i> Add Interface
</button>
<button class="btn btn-success" @click="refreshNetworkData">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
</div>
<div class="card-body">
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>Interface</th>
<th>Status</th>
<th>IP Address</th>
<th>MAC Address</th>
<th>Speed</th>
<th>RX/TX</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="interface in networkInterfaces" :key="interface.id">
<td>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<i class="fas" :class="getInterfaceIcon(interface.type)" style="color: #667eea;"></i>
<strong>{{ interface.name }}</strong>
</div>
</td>
<td>
<span class="status-badge" :class="'status-' + interface.status">
<i class="fas" :class="interface.status === 'up' ? 'fa-arrow-up' : 'fa-arrow-down'"></i>
{{ interface.status }}
</span>
</td>
<td>{{ interface.ip || 'N/A' }}</td>
<td><code>{{ interface.mac || 'N/A' }}</code></td>
<td>{{ interface.speed || 'N/A' }}</td>
<td>
<div style="font-size: 0.75rem;">
<div style="color: #28a745;"><i class="fas fa-arrow-down"></i> {{ formatBytes(interface.rx || 0) }}</div>
<div style="color: #dc3545;"><i class="fas fa-arrow-up"></i> {{ formatBytes(interface.tx || 0) }}</div>
</div>
</td>
<td>
<div class="btn-group">
<button class="btn btn-info btn-sm" @click="configureInterface(interface.id)" title="Configure">
<i class="fas fa-cog"></i>
</button>
<button class="btn btn-sm"
:class="interface.status === 'up' ? 'btn-warning' : 'btn-success'"
@click="toggleInterface(interface.id)"
:title="interface.status === 'up' ? 'Disable' : 'Enable'">
<i class="fas" :class="interface.status === 'up' ? 'fa-stop' : 'fa-play'"></i>
</button>
<button class="btn btn-secondary btn-sm" @click="resetInterface(interface.id)" title="Reset">
<i class="fas fa-redo"></i>
</button>
</div>
</td>
</tr>
<tr v-if="networkInterfaces.length === 0">
<td colspan="7" style="text-align: center; padding: 2rem; color: #6c757d;">
<i class="fas fa-network-wired" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;"></i>
<div>No network interfaces found</div>
<div style="font-size: 0.875rem; margin-top: 0.5rem;">Check network configuration</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Network Configuration -->
<div class="dashboard-grid">
<div class="card">
<div class="card-header">
<h3 class="card-title">DNS Configuration</h3>
<button class="btn btn-primary" @click="showDNSModal = true">
<i class="fas fa-edit"></i> Edit
</button>
</div>
<div class="card-body">
<div class="dns-servers">
<div v-for="(dns, index) in dnsServers" :key="index" class="dns-server-item">
<i class="fas fa-server"></i>
<span>{{ dns }}</span>
<span class="dns-status" :class="getDNSStatus(dns)">{{ getDNSStatusText(dns) }}</span>
</div>
<div v-if="dnsServers.length === 0" class="dns-server-item">
<i class="fas fa-exclamation-triangle" style="color: #ffc107;"></i>
<span>No DNS servers configured</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Firewall (nftables)</h3>
<button class="btn" :class="firewallStatus.enabled ? 'btn-danger' : 'btn-success'" @click="toggleFirewall">
<i class="fas fa-shield-alt"></i>
{{ firewallStatus.enabled ? 'Disable' : 'Enable' }}
</button>
</div>
<div class="card-body">
<div class="firewall-info">
<div class="firewall-stat">
<strong>Status:</strong>
<span :class="firewallStatus.enabled ? 'text-success' : 'text-danger'">
{{ firewallStatus.enabled ? 'Active' : 'Inactive' }}
</span>
</div>
<div class="firewall-stat">
<strong>Active Rules:</strong>
<span>{{ firewallStatus.rules || 0 }}</span>
</div>
<div class="firewall-stat">
<strong>Default Policy:</strong>
<span>{{ firewallStatus.defaultPolicy || 'DROP' }}</span>
</div>
</div>
<button class="btn btn-info" @click="showFirewallRules" style="margin-top: 1rem; width: 100%;">
<i class="fas fa-list"></i> View Rules
</button>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Routing Table</h3>
<button class="btn btn-primary" @click="showRoutingModal = true">
<i class="fas fa-route"></i> Manage
</button>
</div>
<div class="card-body">
<div class="routing-info">
<div class="route-item">
<strong>Default Gateway:</strong>
<span>{{ getDefaultGateway() }}</span>
</div>
<div class="route-item">
<strong>Static Routes:</strong>
<span>{{ getStaticRoutesCount() }}</span>
</div>
<div class="route-item">
<strong>Metric:</strong>
<span>{{ getDefaultMetric() }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Snapshots Section -->
<div class="content-section" :class="{ active: activeSection === 'snapshots' }">
<!-- Snapshot Overview -->
<div class="dashboard-grid" style="margin-bottom: 2rem;">
<div class="card">
<div class="card-header">
<h3 class="card-title">Snapshot Statistics</h3>
<div class="data-source-indicator">
<i class="fas fa-check-circle" style="color: #28a745;"></i>
<span class="data-source-text">Real System ({{ snapshotDataSource || 'snapper' }})</span>
</div>
<i class="fas fa-chart-line"></i>
</div>
<div class="card-body">
<div class="system-stats">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-camera"></i>
</div>
<div class="stat-value">{{ snapshotStats.total || 0 }}</div>
<div class="stat-label">TOTAL</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-calendar-day"></i>
</div>
<div class="stat-value">{{ snapshotStats.today || 0 }}</div>
<div class="stat-label">TODAY</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-hdd"></i>
</div>
<div class="stat-value">{{ formatBytes(snapshotStats.totalSize || 0) }}</div>
<div class="stat-label">SIZE</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Snapshot Schedule</h3>
<i class="fas fa-clock"></i>
</div>
<div class="card-body">
<div class="status-items">
<div class="status-item">
<i class="fas fa-circle status-online"></i>
<span>Hourly Snapshots</span>
<span class="status-text">Active</span>
</div>
<div class="status-item">
<i class="fas fa-circle status-online"></i>
<span>Daily Snapshots</span>
<span class="status-text">Active</span>
</div>
<div class="status-item">
<i class="fas fa-circle status-warning"></i>
<span>Weekly Snapshots</span>
<span class="status-text">Disabled</span>
</div>
</div>
<button class="btn btn-primary" @click="showScheduleModal = true" style="margin-top: 1rem; width: 100%;">
<i class="fas fa-cog"></i> Configure Schedule
</button>
</div>
</div>
</div>
<!-- Snapshot Management -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Snapshot Management</h3>
<div>
<button class="btn btn-success" @click="createSnapshot" style="margin-right: 0.5rem;">
<i class="fas fa-camera"></i> Create Snapshot
</button>
<button class="btn btn-info" @click="createSystemSnapshot" style="margin-right: 0.5rem;">
<i class="fas fa-server"></i> System Snapshot
</button>
<button class="btn btn-primary" @click="refreshSnapshotData">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
</div>
<div class="card-body">
<!-- Snapshot Timeline -->
<div class="snapshot-timeline">
<div v-for="snapshot in snapshots" :key="snapshot.id" class="snapshot-timeline-item">
<div class="snapshot-timeline-marker">
<i class="fas fa-camera"></i>
</div>
<div class="snapshot-timeline-content">
<div class="snapshot-card">
<div class="snapshot-header">
<div class="snapshot-info">
<h4>{{ snapshot.name }}</h4>
<div class="snapshot-meta">
<span class="snapshot-type">{{ snapshot.type || 'manual' }}</span>
<span class="snapshot-date">{{ formatSnapshotDate(snapshot.created) }}</span>
</div>
</div>
<div class="snapshot-size">
{{ formatBytes(snapshot.size || 0) }}
</div>
</div>
<div class="snapshot-details">
<div class="snapshot-description">
{{ snapshot.description || 'No description provided' }}
</div>
<div class="snapshot-path">
<i class="fas fa-folder"></i> {{ snapshot.path || '/system' }}
</div>
</div>
<div class="snapshot-actions">
<button class="btn btn-primary btn-sm" @click="restoreSnapshot(snapshot.id)" title="Restore Snapshot">
<i class="fas fa-undo"></i> Restore
</button>
<button class="btn btn-info btn-sm" @click="cloneSnapshot(snapshot.id)" title="Clone Snapshot">
<i class="fas fa-copy"></i> Clone
</button>
<button class="btn btn-warning btn-sm" @click="exportSnapshot(snapshot.id)" title="Export Snapshot">
<i class="fas fa-download"></i> Export
</button>
<button class="btn btn-danger btn-sm" @click="deleteSnapshot(snapshot.id)" title="Delete Snapshot">
<i class="fas fa-trash"></i> Delete
</button>
</div>
</div>
</div>
</div>
<div v-if="snapshots.length === 0" class="snapshot-timeline-item">
<div class="snapshot-timeline-marker">
<i class="fas fa-camera" style="opacity: 0.3;"></i>
</div>
<div class="snapshot-timeline-content">
<div class="snapshot-card" style="text-align: center; padding: 2rem; color: #6c757d;">
<i class="fas fa-camera" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.3;"></i>
<div>No snapshots found</div>
<div style="font-size: 0.875rem; margin-top: 0.5rem;">Click "Create Snapshot" to get started</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Snapshot Policies -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Retention Policies</h3>
<button class="btn btn-primary" @click="showRetentionModal = true">
<i class="fas fa-cog"></i> Configure
</button>
</div>
<div class="card-body">
<div class="dashboard-grid">
<div class="card retention-policy-card">
<h4>Hourly Snapshots</h4>
<div class="policy-details">
<div>Keep: <strong>24 snapshots</strong></div>
<div>Frequency: <strong>Every hour</strong></div>
<div>Next: <strong>{{ getNextSnapshotTime('hourly') }}</strong></div>
</div>
</div>
<div class="card retention-policy-card">
<h4>Daily Snapshots</h4>
<div class="policy-details">
<div>Keep: <strong>30 snapshots</strong></div>
<div>Frequency: <strong>Daily at 2:00 AM</strong></div>
<div>Next: <strong>{{ getNextSnapshotTime('daily') }}</strong></div>
</div>
</div>
<div class="card retention-policy-card">
<h4>Weekly Snapshots</h4>
<div class="policy-details">
<div>Keep: <strong>12 snapshots</strong></div>
<div>Frequency: <strong>Weekly on Sunday</strong></div>
<div>Status: <strong style="color: #ffc107;">Disabled</strong></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Create Dataset Modal will be dynamically created -->
<!-- Load Vue.js -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<!-- Load v2 Modules -->
<script src="js/auth-v2.js?v=phase2"></script>
<script src="js/core-utils-v2.js?v=phase1"></script>
<script src="js/dashboard-v2.js?v=phase7"></script>
<script src="js/vms-v2.js?v=phase4"></script>
<script src="js/storage-v2.js?v=phase5"></script>
<script src="js/network-v2.js?v=phase7"></script>
<script src="js/snapshots-v2.js?v=phase6"></script>
<script>
// Set authentication for the app
localStorage.setItem('authenticated', 'true');
localStorage.setItem('username', 'root');
console.log('đ JavaScript is working, Vue:', typeof Vue);
const { createApp } = Vue;
// Main PersistenceOS Application with v2 Backend
const PersistenceOSApp = createApp({
data() {
console.log('đ Vue data() function called');
return {
// UI State
activeSection: 'dashboard',
sidebarCollapsed: false,
loading: false,
// System State
systemHealthy: true,
// Dashboard data - connected to v2 modules
systemStats: {
cpu: 0,
memory: 0,
disk: 0,
hostname: 'localhost.localdomain',
version: 'PersistenceOS 6.1.0',
uptime: 0
},
// VM data - connected to v2 modules
vms: [],
vmStats: { running: 0, stopped: 0, total: 0 },
vmDataSource: 'libvirt',
// Storage data - connected to v2 modules
storagePools: [],
datasets: [],
// Network data - connected to v2 modules
networkInterfaces: [],
dnsServers: [],
firewallStatus: { enabled: false, rules: 0 },
// Snapshots data - connected to v2 modules
snapshots: [],
snapshotStats: { total: 0, today: 0, totalSize: 0 },
snapshotDataSource: 'snapper',
// Modal states
showScheduleModal: false,
// New dataset form data
newDataset: {
name: '',
pool: '',
mountpoint: '',
quota: '',
compression: true,
description: ''
}
}
},
computed: {
/**
* Get section title for header
*/
getSectionTitle() {
const titles = {
dashboard: 'System Overview',
virtualization: 'Virtual Machines',
storage: 'Storage Management',
snapshots: 'System Snapshots',
network: 'Network Configuration',
settings: 'System Settings'
};
return titles[this.activeSection] || 'PersistenceOS';
}
},
methods: {
/**
* Toggle sidebar collapsed state
*/
toggleSidebar() {
this.sidebarCollapsed = !this.sidebarCollapsed;
},
/**
* Set active section and refresh data
*/
setActiveSection(section) {
this.activeSection = section;
this.refreshSectionData(section);
},
// Dashboard methods - using v2 modules
async refreshSystemInfo() {
try {
if (window.PersistenceDashboard) {
await window.PersistenceDashboard.refreshSystemInfo();
this.systemStats = window.PersistenceDashboard.getSystemStats();
this.lastUpdated = new Date().toLocaleTimeString();
}
} catch (error) {
console.error('Failed to refresh system info:', error);
}
},
// VM methods - using v2 modules
async refreshVMData() {
try {
if (window.PersistenceVMs) {
await window.PersistenceVMs.refreshVMs();
this.vms = window.PersistenceVMs.getVMs();
this.vmStats = window.PersistenceVMs.getVMStats();
this.vmDataSource = window.PersistenceVMs.data?.source || 'libvirt';
}
} catch (error) {
console.error('Failed to refresh VM data:', error);
}
},
async startVM(id) {
try {
if (window.PersistenceVMs) {
await window.PersistenceVMs.startVM(id);
await this.refreshVMData();
this.showNotification('VM started successfully', 'success');
}
} catch (error) {
console.error('Failed to start VM:', error);
this.showNotification('Failed to start VM: ' + error.message, 'error');
}
},
async stopVM(id) {
try {
if (window.PersistenceVMs) {
await window.PersistenceVMs.stopVM(id);
await this.refreshVMData();
this.showNotification('VM stopped successfully', 'success');
}
} catch (error) {
console.error('Failed to stop VM:', error);
this.showNotification('Failed to stop VM: ' + error.message, 'error');
}
},
async deleteVM(id) {
if (confirm('Are you sure you want to delete this VM?')) {
try {
if (window.PersistenceVMs) {
await window.PersistenceVMs.deleteVM(id);
await this.refreshVMData();
this.showNotification('VM deleted successfully', 'success');
}
} catch (error) {
console.error('Failed to delete VM:', error);
this.showNotification('Failed to delete VM: ' + error.message, 'error');
}
}
},
async rebootVM(id) {
try {
if (window.PersistenceVMs) {
await window.PersistenceVMs.rebootVM(id);
await this.refreshVMData();
this.showNotification('VM rebooted successfully', 'success');
}
} catch (error) {
console.error('Failed to reboot VM:', error);
this.showNotification('Failed to reboot VM: ' + error.message, 'error');
}
},
async editVM(id) {
// TODO: Implement VM editing modal
this.showNotification('VM editing feature coming soon', 'info');
},
// Storage methods - using v2 modules
async refreshStorageData() {
try {
if (window.PersistenceStorage) {
await window.PersistenceStorage.refreshStorage();
this.storagePools = window.PersistenceStorage.getPools();
this.datasets = window.PersistenceStorage.getDatasets();
}
} catch (error) {
console.error('Failed to refresh storage data:', error);
}
},
/**
* Create a new storage pool using the working modal pattern
*/
showCreatePoolModal() {
console.log('đ¯ Opening Create Storage Pool Modal');
// Create modal HTML using the working pattern from dataset creation
const modalHtml = `
<div id="createPoolModal" class="modal-overlay" style="display: flex;">
<div class="modal-content" style="max-width: 800px; width: 90%; max-height: 90vh; overflow-y: auto;">
<div class="modal-header">
<h3><i class="fas fa-plus"></i> Create Advanced Storage Pool</h3>
<button type="button" class="btn-close" onclick="closeCreatePoolModal()">×</button>
</div>
<div class="modal-body">
<form id="createPoolForm">
<!-- Basic Configuration -->
<div class="form-section">
<h4><i class="fas fa-cog"></i> Basic Configuration</h4>
<div class="form-group">
<label for="pool-name">Pool Name *</label>
<input type="text" id="pool-name" name="name" class="form-control" required
placeholder="Enter pool name (e.g., vm-storage)">
</div>
<div class="form-group">
<label for="pool-type">Storage Type *</label>
<select id="pool-type" name="storage_type" class="form-control" required onchange="updatePoolTypeOptions()">
<option value="">Select storage configuration</option>
<option value="single-btrfs">Single Disk - BTRFS (Snapshots, Compression)</option>
<option value="single-xfs">Single Disk - XFS (High Performance)</option>
<option value="single-ext4">Single Disk - EXT4 (Stable, Compatible)</option>
<option value="btrfs-raid0">BTRFS RAID 0 - Striped (Performance)</option>
<option value="btrfs-raid1">BTRFS RAID 1 - Mirrored (Redundancy)</option>
<option value="btrfs-raid10">BTRFS RAID 10 - Striped Mirror (Performance + Redundancy)</option>
<option value="mdadm-raid0">Software RAID 0 - Striped (Maximum Performance)</option>
<option value="mdadm-raid1">Software RAID 1 - Mirrored (Data Protection)</option>
<option value="mdadm-raid5">Software RAID 5 - Parity (Space Efficient)</option>
<option value="mdadm-raid6">Software RAID 6 - Dual Parity (High Protection)</option>
<option value="mdadm-raid10">Software RAID 10 - Striped Mirror (Enterprise)</option>
<option value="lvm-stripe">LVM Striped - Logical Volume Performance</option>
<option value="lvm-mirror">LVM Mirror - Logical Volume Redundancy</option>
</select>
<small class="form-text">Choose storage configuration based on your performance and redundancy needs</small>
</div>
<!-- Device Selection -->
<div class="form-group">
<label for="pool-devices">Storage Devices *</label>
<div id="device-selection-area">
<div class="device-input-group">
<select id="pool-device-1" name="devices[]" class="form-control device-select" required>
<option value="">Select primary device...</option>
<option value="loading">Loading available devices...</option>
</select>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeDeviceInput(this)" style="display: none; margin-left: 0.5rem;">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div style="margin-top: 0.5rem;">
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addDeviceInput()" id="add-device-btn" style="display: none;">
<i class="fas fa-plus"></i> Add Device
</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="detectDevices()">
<i class="fas fa-search"></i> Detect Available Devices
</button>
<button type="button" class="btn btn-sm btn-info" onclick="refreshDeviceList()" style="margin-left: 0.5rem;">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
<small class="form-text" id="device-help-text">Single device required. Additional devices will be shown based on RAID selection.</small>
</div>
</div>
<!-- Advanced Options -->
<div class="form-section" id="advanced-options" style="display: none;">
<h4><i class="fas fa-cogs"></i> Advanced Options</h4>
<!-- BTRFS Options -->
<div id="btrfs-options" style="display: none;">
<div class="form-group">
<label>
<input type="checkbox" name="enable_compression"> Enable Compression
</label>
<select name="compression_type" class="form-control" style="margin-top: 0.5rem;">
<option value="zstd">ZSTD (Recommended)</option>
<option value="lzo">LZO (Fast)</option>
<option value="zlib">ZLIB (High Compression)</option>
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="enable_snapshots" checked> Enable Snapshots
</label>
</div>
</div>
<!-- VM Integration -->
<div class="form-group">
<label>
<input type="checkbox" name="create_vm_directories" checked> Create VM Directory Structure
</label>
<small class="form-text">Creates /vms, /iso, /templates, /backups directories</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="enable_libvirt_pool" checked> Register with Libvirt
</label>
<small class="form-text">Automatically register storage pool with libvirt for VM management</small>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeCreatePoolModal()">
<i class="fas fa-times"></i> Cancel
</button>
<button type="button" class="btn btn-primary" onclick="createPoolFromModal()">
<i class="fas fa-plus"></i> Create Pool
</button>
</div>
</div>
</div>
`;
// Add modal to DOM
document.body.insertAdjacentHTML('beforeend', modalHtml);
document.body.classList.add('modal-open');
// Initialize device selection and focus on name field
setTimeout(async () => {
document.getElementById('pool-name').focus();
await window.initializeDeviceSelection();
}, 100);
},
/**
* Fallback createPool method that calls the modal
*/
async createPool() {
this.showCreatePoolModal();
},
/**
* Create a new virtual machine using the working modal pattern
*/
showCreateVMModal() {
console.log('đ¯ Opening Create VM Modal');
// Create modal HTML using the working pattern
const modalHtml = `
<div id="createVMModal" class="modal-overlay" style="display: flex;">
<div class="modal-content" style="max-width: 1100px; width: 98%; max-height: 95vh; overflow-y: auto;">
<div class="modal-header">
<h3><i class="fas fa-plus"></i> Create Virtual Machine</h3>
<button type="button" class="btn-close" onclick="closeCreateVMModal()">×</button>
</div>
<div class="modal-body">
<form id="createVMForm">
<!-- Basic Configuration -->
<div class="form-section">
<h4><i class="fas fa-cog"></i> Basic Configuration</h4>
<div class="form-group">
<label for="vm-name">VM Name *</label>
<input type="text" id="vm-name" name="name" class="form-control" required
placeholder="Enter VM name (e.g., ubuntu-server)">
</div>
<div class="form-group">
<label for="vm-os-type">Operating System *</label>
<select id="vm-os-type" name="os_type" class="form-control" required onchange="updateOSDefaults()">
<option value="">Select operating system</option>
<option value="linux">Linux (Ubuntu, CentOS, SUSE, etc.)</option>
<option value="windows">Windows (10, 11, Server)</option>
<option value="other">Other / Custom</option>
</select>
</div>
</div>
<!-- Hardware Configuration -->
<div class="form-section">
<h4><i class="fas fa-microchip"></i> Hardware Configuration</h4>
<div class="form-row">
<div class="form-group" style="flex: 1; margin-right: 1rem;">
<label for="vm-cpu-cores">CPU Cores *</label>
<input type="number" id="vm-cpu-cores" name="cpu_cores" class="form-control"
required min="1" max="64" value="2"
placeholder="Enter number of CPU cores">
<small class="form-text">Recommended: 1-4 cores for most VMs</small>
</div>
<div class="form-group" style="flex: 1;">
<label for="vm-cpu-topology">CPU Topology</label>
<select id="vm-cpu-topology" name="cpu_topology" class="form-control">
<option value="auto">Auto (Recommended)</option>
<option value="1:2:1">1 Socket, 2 Cores, 1 Thread</option>
<option value="1:4:1">1 Socket, 4 Cores, 1 Thread</option>
<option value="2:2:1">2 Sockets, 2 Cores, 1 Thread</option>
<option value="1:2:2">1 Socket, 2 Cores, 2 Threads (SMT)</option>
</select>
<small class="form-text">Advanced CPU configuration</small>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex: 1; margin-right: 1rem;">
<label for="vm-memory">Memory (GB) *</label>
<input type="number" id="vm-memory" name="memory_gb" class="form-control"
required min="1" max="256" value="4" step="0.5"
placeholder="Enter memory in GB">
<small class="form-text">Recommended: 2-8 GB for most VMs</small>
</div>
<div class="form-group" style="flex: 1;">
<label for="vm-memory-balloon">Memory Ballooning</label>
<select id="vm-memory-balloon" name="memory_balloon" class="form-control">
<option value="enabled">Enabled (Recommended)</option>
<option value="disabled">Disabled</option>
</select>
<small class="form-text">Dynamic memory management</small>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex: 1; margin-right: 1rem;">
<label for="vm-disk-size">Disk Size (GB) *</label>
<input type="number" id="vm-disk-size" name="disk_size_gb" class="form-control"
required min="10" max="10000" value="40"
placeholder="Enter disk size in GB">
<small class="form-text">Recommended: 20-100 GB for most VMs. Minimum: 10 GB</small>
</div>
<div class="form-group" style="flex: 1;">
<label for="vm-disk-type">Disk Type</label>
<select id="vm-disk-type" name="disk_type" class="form-control">
<option value="qcow2">QCOW2 (Recommended)</option>
<option value="raw">RAW (Better Performance)</option>
<option value="qcow2-compressed">QCOW2 Compressed</option>
</select>
<small class="form-text">Disk image format</small>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex: 1; margin-right: 1rem;">
<label for="vm-disk-cache">Disk Cache Mode</label>
<select id="vm-disk-cache" name="disk_cache" class="form-control">
<option value="writeback">Writeback (Default)</option>
<option value="writethrough">Writethrough (Safer)</option>
<option value="none">None (Best Performance)</option>
<option value="directsync">Direct Sync</option>
</select>
<small class="form-text">Disk caching strategy</small>
</div>
<div class="form-group" style="flex: 1;">
<label for="vm-disk-io">Disk I/O Mode</label>
<select id="vm-disk-io" name="disk_io" class="form-control">
<option value="threads">Threads (Default)</option>
<option value="native">Native (Better Performance)</option>
<option value="io_uring">io_uring (Latest)</option>
</select>
<small class="form-text">Disk I/O threading mode</small>
</div>
</div>
</div>
<!-- Installation Method -->
<div class="form-section">
<h4><i class="fas fa-compact-disc"></i> Installation Method</h4>
<div class="form-group">
<label for="vm-install-method">Installation Method *</label>
<select id="vm-install-method" name="install_method" class="form-control" required onchange="updateInstallMethodOptions()">
<option value="">Select installation method</option>
<option value="iso">ISO Image (from server)</option>
<option value="iso-upload">Upload ISO from local drive</option>
<option value="network">Network Install (PXE)</option>
<option value="template">From VM Template</option>
<option value="blank">Create Blank VM</option>
</select>
</div>
<!-- ISO Upload Section -->
<div id="iso-upload-section" class="form-group" style="display: none;">
<label for="vm-iso-file">ISO File</label>
<input type="file" id="vm-iso-file" name="iso_file" class="form-control" accept=".iso,.img">
<small class="form-text">Select an ISO file to upload for VM installation</small>
</div>
<!-- Template Selection -->
<div id="template-section" class="form-group" style="display: none;">
<label for="vm-template">VM Template</label>
<select id="vm-template" name="template_id" class="form-control">
<option value="">Loading templates...</option>
</select>
</div>
</div>
<!-- Advanced Options -->
<div class="form-section">
<h4><i class="fas fa-cogs"></i> Advanced Options</h4>
<div class="form-group">
<label for="vm-storage-pool">Storage Pool</label>
<select id="vm-storage-pool" name="storage_pool" class="form-control">
<option value="">Default (libvirt images)</option>
</select>
<small class="form-text">Select storage pool for VM disk placement</small>
</div>
<div class="form-row">
<div class="form-group" style="flex: 1; margin-right: 1rem;">
<label for="vm-boot-firmware">Boot Firmware</label>
<select id="vm-boot-firmware" name="boot_firmware" class="form-control">
<option value="uefi" selected>UEFI (Recommended)</option>
<option value="bios">Legacy BIOS</option>
</select>
</div>
<div class="form-group" style="flex: 1;">
<label for="vm-system-clock">System Clock</label>
<select id="vm-system-clock" name="system_clock" class="form-control">
<option value="utc">UTC (Recommended)</option>
<option value="local">Local Time</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex: 1; margin-right: 1rem;">
<label for="vm-network-model">Network Model</label>
<select id="vm-network-model" name="network_model" class="form-control">
<option value="virtio">VirtIO (Recommended)</option>
<option value="e1000">Intel E1000</option>
<option value="rtl8139">Realtek RTL8139</option>
</select>
<small class="form-text">Network adapter type</small>
</div>
<div class="form-group" style="flex: 1;">
<label for="vm-graphics">Graphics Type</label>
<select id="vm-graphics" name="graphics_type" class="form-control">
<option value="vnc">VNC (Recommended)</option>
<option value="none">Headless (No Graphics)</option>
</select>
<small class="form-text">Console access method</small>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex: 1; margin-right: 1rem;">
<label for="vm-network-bridge">Network Bridge</label>
<select id="vm-network-bridge" name="network_bridge" class="form-control">
<option value="virbr0">Default Bridge (virbr0)</option>
<option value="br0">Physical Bridge (br0)</option>
<option value="br1">Bridge br1</option>
</select>
<small class="form-text">Network connection bridge</small>
</div>
<div class="form-group" style="flex: 1;">
<label for="vm-vlan-id">VLAN ID (Optional)</label>
<input type="number" id="vm-vlan-id" name="vlan_id" class="form-control"
min="1" max="4094" placeholder="Leave empty for no VLAN">
<small class="form-text">VLAN tag for network isolation</small>
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex: 1; margin-right: 1rem; position: relative;">
<label for="vm-cpu-mode">CPU Mode</label>
<select id="vm-cpu-mode" name="cpu_mode" class="form-control">
<option value="host-model">Host Model (Recommended)</option>
<option value="host-passthrough">Host Passthrough (Best Performance)</option>
</select>
<small class="form-text">CPU virtualization mode</small>
<div id="cpu-mode-tooltip" class="vm-tooltip"></div>
</div>
<div class="form-group" style="flex: 1;">
<label for="vm-machine-type">Machine Type</label>
<select id="vm-machine-type" name="machine_type" class="form-control">
<option value="q35">Q35 (Modern, Recommended)</option>
<option value="pc">PC (Legacy)</option>
<option value="virt">ARM Virtual Machine</option>
</select>
<small class="form-text">Virtual machine chipset</small>
</div>
</div>
<div class="form-group">
<label>Performance & Features:</label>
<div style="margin-left: 1rem;">
<label style="display: block; margin-bottom: 0.5rem;">
<input type="checkbox" name="enable_virtio" checked> Enable VirtIO drivers for better performance
</label>
<label style="display: block; margin-bottom: 0.5rem;">
<input type="checkbox" name="enable_kvm" checked> Enable KVM hardware acceleration
</label>
<label style="display: block; margin-bottom: 0.5rem;">
<input type="checkbox" name="enable_hugepages"> Use huge pages for memory (Advanced)
</label>
<label style="display: block; margin-bottom: 0.5rem;">
<input type="checkbox" name="enable_numa"> Enable NUMA topology
</label>
<label style="display: block; margin-bottom: 0.5rem;">
<input type="checkbox" name="start_after_creation" checked> Start VM after creation
</label>
</div>
</div>
<!-- Future Features Note -->
<div class="form-group">
<div class="alert alert-info" style="margin: 0; padding: 1rem; background-color: #e3f2fd; border: 1px solid #90caf9; border-radius: 6px;">
<div style="display: flex; align-items: flex-start; gap: 0.75rem;">
<i class="fas fa-info-circle" style="color: #1976d2; margin-top: 0.125rem; font-size: 1.1rem;"></i>
<div>
<strong style="color: #1565c0; font-size: 0.95rem;">Advanced Features Coming Soon</strong>
<div style="color: #424242; font-size: 0.875rem; margin-top: 0.25rem; line-height: 1.4;">
<strong>GPU Passthrough</strong> and <strong>PCI Device Passthrough</strong> will be available in a future PersistenceOS release.
Current VM creation supports all standard virtualization needs including high-performance configurations.
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeCreateVMModal()">
<i class="fas fa-times"></i> Cancel
</button>
<button type="button" class="btn btn-primary" onclick="createVMFromModal()">
<i class="fas fa-plus"></i> Create VM
</button>
</div>
</div>
</div>
`;
// Add modal to DOM
document.body.insertAdjacentHTML('beforeend', modalHtml);
document.body.classList.add('modal-open');
// Load storage pools and templates
this.loadVMCreationData();
// Focus on name field
setTimeout(() => {
document.getElementById('vm-name').focus();
}, 100);
},
/**
* Show VM console preview window (Enhanced VMware ESXi style)
*/
showVMConsole(vmId, vmName) {
console.log(`đĨī¸ Opening enhanced console preview for VM: ${vmName} (${vmId})`);
// Create enhanced console preview modal
const modalHtml = `
<div id="vmConsoleModal" class="modal-overlay" style="display: flex;">
<div class="modal-content vm-console-modal" style="max-width: 1400px; width: 98%; max-height: 98vh;">
<div class="modal-header">
<h3><i class="fas fa-desktop"></i> VM Console - ${vmName}</h3>
<div class="console-controls">
<button type="button" class="btn btn-sm btn-success" onclick="takeVMScreenshot('${vmId}')" title="Take Screenshot">
<i class="fas fa-camera"></i>
</button>
<button type="button" class="btn btn-sm btn-info" onclick="showVMPerformance('${vmId}')" title="Performance Monitor">
<i class="fas fa-chart-line"></i>
</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleConsoleSize()" title="Toggle Size">
<i class="fas fa-expand-arrows-alt"></i>
</button>
<button type="button" class="btn btn-sm btn-warning" onclick="refreshVMConsole('${vmId}')" title="Refresh">
<i class="fas fa-sync-alt"></i>
</button>
<button type="button" class="btn-close" onclick="closeVMConsole()">×</button>
</div>
</div>
<div class="modal-body console-body" style="display: flex; gap: 1rem;">
<!-- Main Console Area -->
<div class="vm-console-container" id="vmConsoleContainer" style="flex: 1;">
<div class="console-loading">
<i class="fas fa-spinner fa-spin"></i>
<p>Connecting to VM console...</p>
</div>
<div class="console-preview" id="consolePreview" style="display: none;">
<canvas id="vncCanvas" width="1024" height="768"></canvas>
</div>
<div class="console-info" id="consoleInfo">
<div class="info-item">
<strong>VM Name:</strong> ${vmName}
</div>
<div class="info-item">
<strong>Status:</strong> <span id="vmStatus">Checking...</span>
</div>
<div class="info-item">
<strong>Console:</strong> <span id="consoleStatus">Connecting...</span>
</div>
</div>
</div>
<!-- Performance Sidebar -->
<div class="vm-performance-sidebar" id="vmPerformanceSidebar" style="width: 300px; background: #2d2d2d; border-radius: 8px; padding: 1rem; display: none;">
<h4 style="color: #ffffff; margin-bottom: 1rem;"><i class="fas fa-chart-line"></i> Performance</h4>
<!-- CPU Usage -->
<div class="performance-metric" style="margin-bottom: 1rem;">
<label style="color: #cccccc; font-size: 0.9rem;">CPU Usage</label>
<div class="progress-bar" style="background: #444; height: 20px; border-radius: 10px; overflow: hidden; margin: 0.5rem 0;">
<div id="vmCpuProgress" class="progress-fill" style="height: 100%; background: linear-gradient(90deg, #28a745, #ffc107, #dc3545); width: 0%; transition: width 0.3s;"></div>
</div>
<span id="vmCpuValue" style="color: #ffffff; font-size: 0.9rem;">0%</span>
</div>
<!-- Memory Usage -->
<div class="performance-metric" style="margin-bottom: 1rem;">
<label style="color: #cccccc; font-size: 0.9rem;">Memory Usage</label>
<div class="progress-bar" style="background: #444; height: 20px; border-radius: 10px; overflow: hidden; margin: 0.5rem 0;">
<div id="vmMemoryProgress" class="progress-fill" style="height: 100%; background: linear-gradient(90deg, #17a2b8, #ffc107, #dc3545); width: 0%; transition: width 0.3s;"></div>
</div>
<span id="vmMemoryValue" style="color: #ffffff; font-size: 0.9rem;">0 MB / 0 MB</span>
</div>
<!-- Network I/O -->
<div class="performance-metric" style="margin-bottom: 1rem;">
<label style="color: #cccccc; font-size: 0.9rem;">Network I/O</label>
<div style="display: flex; justify-content: space-between; margin: 0.5rem 0;">
<span style="color: #28a745; font-size: 0.8rem;">â <span id="vmNetworkRx">0 KB/s</span></span>
<span style="color: #dc3545; font-size: 0.8rem;">â <span id="vmNetworkTx">0 KB/s</span></span>
</div>
</div>
<!-- Disk I/O -->
<div class="performance-metric" style="margin-bottom: 1rem;">
<label style="color: #cccccc; font-size: 0.9rem;">Disk I/O</label>
<div style="display: flex; justify-content: space-between; margin: 0.5rem 0;">
<span style="color: #17a2b8; font-size: 0.8rem;">R: <span id="vmDiskRead">0 KB/s</span></span>
<span style="color: #ffc107; font-size: 0.8rem;">W: <span id="vmDiskWrite">0 KB/s</span></span>
</div>
</div>
<!-- VM Details -->
<div class="vm-details" style="border-top: 1px solid #444; padding-top: 1rem; margin-top: 1rem;">
<h5 style="color: #ffffff; margin-bottom: 0.5rem;">VM Details</h5>
<div style="font-size: 0.8rem; color: #cccccc;">
<div>Uptime: <span id="vmUptime">Unknown</span></div>
<div>vCPUs: <span id="vmVcpus">Unknown</span></div>
<div>Memory: <span id="vmMemoryTotal">Unknown</span></div>
<div>OS: <span id="vmOsType">Unknown</span></div>
</div>
</div>
</div>
</div>
<div class="modal-footer console-footer">
<div class="console-stats">
<span id="consoleResolution">Resolution: 1024x768</span>
<span id="consoleConnection">Connection: Initializing</span>
<span id="consoleFps">FPS: 0</span>
</div>
<div class="console-actions">
<button type="button" class="btn btn-info btn-sm" onclick="sendSpecialKey('${vmId}', 'alt-tab')" title="Alt+Tab">
<i class="fas fa-window-restore"></i> Alt+Tab
</button>
<button type="button" class="btn btn-warning btn-sm" onclick="sendCtrlAltDel('${vmId}')">
<i class="fas fa-keyboard"></i> Ctrl+Alt+Del
</button>
<button type="button" class="btn btn-success btn-sm" onclick="createVMSnapshot('${vmId}')" title="Create Snapshot">
<i class="fas fa-save"></i> Snapshot
</button>
<button type="button" class="btn btn-secondary" onclick="closeVMConsole()">
<i class="fas fa-times"></i> Close
</button>
</div>
</div>
</div>
</div>
`;
// Add modal to DOM
document.body.insertAdjacentHTML('beforeend', modalHtml);
document.body.classList.add('modal-open');
// Initialize console connection
this.initializeVMConsole(vmId, vmName);
},
/**
* Initialize VM console connection with enhanced features
*/
async initializeVMConsole(vmId, vmName) {
try {
// Get VM console information
const response = await fetch(`/api/vms/${vmId}/console`);
if (response.ok) {
const consoleData = await response.json();
// Update console info
document.getElementById('vmStatus').textContent = consoleData.vm_status || 'Unknown';
document.getElementById('consoleStatus').textContent = consoleData.vnc_available ? 'Available' : 'Unavailable';
// Update VM details in performance sidebar
if (consoleData.vm_details) {
document.getElementById('vmVcpus').textContent = consoleData.vm_details.vcpus || 'Unknown';
document.getElementById('vmMemoryTotal').textContent = consoleData.vm_details.memory || 'Unknown';
document.getElementById('vmOsType').textContent = consoleData.vm_details.os_type || 'Unknown';
document.getElementById('vmUptime').textContent = consoleData.vm_details.uptime || 'Unknown';
}
if (consoleData.vnc_available && consoleData.vnc_port) {
// Initialize enhanced VNC connection
this.connectEnhancedVNC(vmId, consoleData.vnc_port);
// Start performance monitoring
this.startVMPerformanceMonitoring(vmId);
} else {
// Show console unavailable message
this.showConsoleUnavailable(vmId, vmName);
}
} else {
throw new Error('Failed to get console information');
}
} catch (error) {
console.error('Failed to initialize VM console:', error);
this.showConsoleError(error.message);
}
},
/**
* Connect to enhanced VNC console with performance features
*/
connectEnhancedVNC(vmId, vncPort) {
const canvas = document.getElementById('vncCanvas');
const consolePreview = document.getElementById('consolePreview');
const consoleLoading = document.querySelector('.console-loading');
if (!canvas) return;
try {
// Show enhanced simulated console preview
this.showEnhancedSimulatedConsole(vmId, vncPort);
// Hide loading, show preview
consoleLoading.style.display = 'none';
consolePreview.style.display = 'block';
// Update connection status with enhanced info
document.getElementById('consoleConnection').textContent = `Connected (Port: ${vncPort})`;
// Start FPS counter simulation
this.startFPSCounter();
// Add enhanced keyboard shortcuts
this.setupEnhancedKeyboardHandlers(vmId);
} catch (error) {
console.error('Enhanced VNC connection failed:', error);
this.showConsoleError('Enhanced VNC connection failed');
}
},
/**
* Start VM performance monitoring
*/
startVMPerformanceMonitoring(vmId) {
// Clear any existing monitoring interval
if (this.performanceInterval) {
clearInterval(this.performanceInterval);
}
// Start performance monitoring every 2 seconds
this.performanceInterval = setInterval(async () => {
try {
const response = await fetch(`/api/vms/${vmId}/performance`);
if (response.ok) {
const perfData = await response.json();
this.updatePerformanceDisplay(perfData);
} else {
// Simulate performance data for demo
this.simulatePerformanceData();
}
} catch (error) {
// Fallback to simulated data
this.simulatePerformanceData();
}
}, 2000);
},
/**
* Update performance display with real data
*/
updatePerformanceDisplay(perfData) {
// Update CPU usage
const cpuUsage = perfData.cpu_usage || 0;
document.getElementById('vmCpuProgress').style.width = `${cpuUsage}%`;
document.getElementById('vmCpuValue').textContent = `${cpuUsage.toFixed(1)}%`;
// Update Memory usage
const memoryUsed = perfData.memory_used || 0;
const memoryTotal = perfData.memory_total || 4096;
const memoryPercent = (memoryUsed / memoryTotal) * 100;
document.getElementById('vmMemoryProgress').style.width = `${memoryPercent}%`;
document.getElementById('vmMemoryValue').textContent = `${memoryUsed} MB / ${memoryTotal} MB`;
// Update Network I/O
document.getElementById('vmNetworkRx').textContent = this.formatBytes(perfData.network_rx_rate || 0) + '/s';
document.getElementById('vmNetworkTx').textContent = this.formatBytes(perfData.network_tx_rate || 0) + '/s';
// Update Disk I/O
document.getElementById('vmDiskRead').textContent = this.formatBytes(perfData.disk_read_rate || 0) + '/s';
document.getElementById('vmDiskWrite').textContent = this.formatBytes(perfData.disk_write_rate || 0) + '/s';
},
/**
* Simulate performance data for demo purposes
*/
simulatePerformanceData() {
const cpuUsage = Math.random() * 100;
const memoryUsed = 1024 + Math.random() * 2048;
const memoryTotal = 4096;
const networkRx = Math.random() * 1024 * 1024; // Random bytes/sec
const networkTx = Math.random() * 512 * 1024;
const diskRead = Math.random() * 10 * 1024 * 1024;
const diskWrite = Math.random() * 5 * 1024 * 1024;
this.updatePerformanceDisplay({
cpu_usage: cpuUsage,
memory_used: memoryUsed,
memory_total: memoryTotal,
network_rx_rate: networkRx,
network_tx_rate: networkTx,
disk_read_rate: diskRead,
disk_write_rate: diskWrite
});
},
/**
* Show enhanced simulated console with performance features
*/
showEnhancedSimulatedConsole(vmId, vncPort) {
const canvas = document.getElementById('vncCanvas');
const ctx = canvas.getContext('2d');
// Clear canvas with gradient background
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#001122');
gradient.addColorStop(1, '#000000');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw enhanced console content
ctx.fillStyle = '#00ff00';
ctx.font = 'bold 16px monospace';
ctx.fillText(`PersistenceOS Enhanced VM Console - ${vmId}`, 20, 30);
ctx.font = '14px monospace';
ctx.fillStyle = '#00ccff';
ctx.fillText(`VNC Port: ${vncPort} | Resolution: 1024x768 | Enhanced Mode`, 20, 60);
ctx.fillStyle = '#ffff00';
ctx.fillText('Enhanced Features Active:', 20, 100);
ctx.fillStyle = '#ffffff';
ctx.fillText('âĸ Real-time Performance Monitoring', 40, 130);
ctx.fillText('âĸ Advanced Keyboard Shortcuts', 40, 150);
ctx.fillText('âĸ Screenshot Capture', 40, 170);
ctx.fillText('âĸ VM Snapshot Integration', 40, 190);
ctx.fillStyle = '#00ff00';
ctx.fillText('Click to interact with VM (Enhanced Mode)', 20, 230);
// Draw performance indicators
this.drawPerformanceIndicators(ctx);
// Draw enhanced border with corners
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 2;
ctx.strokeRect(10, 10, canvas.width - 20, canvas.height - 20);
// Draw corner indicators
ctx.fillStyle = '#00ff00';
const cornerSize = 10;
// Top-left
ctx.fillRect(10, 10, cornerSize, 2);
ctx.fillRect(10, 10, 2, cornerSize);
// Top-right
ctx.fillRect(canvas.width - 20, 10, cornerSize, 2);
ctx.fillRect(canvas.width - 12, 10, 2, cornerSize);
// Bottom-left
ctx.fillRect(10, canvas.height - 12, cornerSize, 2);
ctx.fillRect(10, canvas.height - 20, 2, cornerSize);
// Bottom-right
ctx.fillRect(canvas.width - 20, canvas.height - 12, cornerSize, 2);
ctx.fillRect(canvas.width - 12, canvas.height - 20, 2, cornerSize);
// Add enhanced click handler
canvas.onclick = (event) => {
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.showNotification(`Enhanced VM console interaction at (${Math.round(x)}, ${Math.round(y)})`, 'info');
// Simulate click effect
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
ctx.beginPath();
ctx.arc(x, y, 20, 0, 2 * Math.PI);
ctx.fill();
setTimeout(() => {
this.showEnhancedSimulatedConsole(vmId, vncPort);
}, 200);
};
},
/**
* Draw performance indicators on console
*/
drawPerformanceIndicators(ctx) {
const startY = 280;
const barWidth = 200;
const barHeight = 15;
// CPU indicator
ctx.fillStyle = '#666666';
ctx.fillRect(20, startY, barWidth, barHeight);
ctx.fillStyle = '#ff6b6b';
ctx.fillRect(20, startY, barWidth * 0.45, barHeight);
ctx.fillStyle = '#ffffff';
ctx.font = '12px monospace';
ctx.fillText('CPU: 45%', 230, startY + 12);
// Memory indicator
ctx.fillStyle = '#666666';
ctx.fillRect(20, startY + 25, barWidth, barHeight);
ctx.fillStyle = '#4ecdc4';
ctx.fillRect(20, startY + 25, barWidth * 0.62, barHeight);
ctx.fillText('Memory: 62%', 230, startY + 37);
// Network indicator
ctx.fillStyle = '#666666';
ctx.fillRect(20, startY + 50, barWidth, barHeight);
ctx.fillStyle = '#45b7d1';
ctx.fillRect(20, startY + 50, barWidth * 0.28, barHeight);
ctx.fillText('Network: 28%', 230, startY + 62);
},
/**
* Show console unavailable message
*/
showConsoleUnavailable(vmId, vmName) {
const consoleContainer = document.getElementById('vmConsoleContainer');
consoleContainer.innerHTML = `
<div class="console-unavailable">
<i class="fas fa-exclamation-triangle"></i>
<h4>Console Unavailable</h4>
<p>The console for VM "${vmName}" is not available.</p>
<p>This may be because:</p>
<ul>
<li>The VM is not running</li>
<li>VNC is not enabled</li>
<li>Network connectivity issues</li>
</ul>
<button class="btn btn-primary" onclick="refreshVMConsole('${vmId}')">
<i class="fas fa-sync-alt"></i> Retry Connection
</button>
</div>
`;
},
/**
* Show console error
*/
showConsoleError(errorMessage) {
const consoleContainer = document.getElementById('vmConsoleContainer');
consoleContainer.innerHTML = `
<div class="console-error">
<i class="fas fa-times-circle"></i>
<h4>Console Error</h4>
<p>Failed to connect to VM console:</p>
<p class="error-message">${errorMessage}</p>
<button class="btn btn-primary" onclick="closeVMConsole()">
<i class="fas fa-times"></i> Close
</button>
</div>
`;
},
/**
* Start FPS counter for console display
*/
startFPSCounter() {
let frameCount = 0;
let lastTime = Date.now();
const updateFPS = () => {
frameCount++;
const currentTime = Date.now();
if (currentTime - lastTime >= 1000) {
const fps = Math.round(frameCount * 1000 / (currentTime - lastTime));
const fpsElement = document.getElementById('consoleFps');
if (fpsElement) {
fpsElement.textContent = `FPS: ${fps}`;
}
frameCount = 0;
lastTime = currentTime;
}
if (document.getElementById('vmConsoleModal')) {
requestAnimationFrame(updateFPS);
}
};
requestAnimationFrame(updateFPS);
},
/**
* Setup enhanced keyboard handlers for console
*/
setupEnhancedKeyboardHandlers(vmId) {
const canvas = document.getElementById('vncCanvas');
if (!canvas) return;
canvas.addEventListener('keydown', (event) => {
// Handle special key combinations
if (event.ctrlKey && event.altKey && event.key === 'Delete') {
event.preventDefault();
this.sendSpecialKey(vmId, 'ctrl-alt-del');
} else if (event.altKey && event.key === 'Tab') {
event.preventDefault();
this.sendSpecialKey(vmId, 'alt-tab');
}
});
// Make canvas focusable for keyboard events
canvas.tabIndex = 0;
canvas.focus();
},
/**
* Send special key combination to VM
*/
async sendSpecialKey(vmId, keyCombo) {
try {
const response = await fetch(`/api/vms/${vmId}/send-key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key_combination: keyCombo })
});
if (response.ok) {
this.showNotification(`${keyCombo.toUpperCase()} sent to VM`, 'success');
} else {
throw new Error('Failed to send key combination');
}
} catch (error) {
console.error(`Failed to send ${keyCombo}:`, error);
this.showNotification(`Failed to send ${keyCombo}`, 'error');
}
},
/**
* Format bytes for display
*/
formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
/**
* Load data needed for VM creation
*/
async loadVMCreationData() {
// Load storage pools
try {
const response = await fetch('/api/storage/pools');
if (response.ok) {
const data = await response.json();
const storagePoolSelect = document.getElementById('vm-storage-pool');
if (storagePoolSelect && data.pools) {
// Clear existing options except default
storagePoolSelect.innerHTML = '<option value="">Default (libvirt images)</option>';
data.pools.forEach(pool => {
const option = document.createElement('option');
option.value = pool.name;
option.textContent = `${pool.name} (${pool.type})`;
storagePoolSelect.appendChild(option);
});
}
}
} catch (error) {
console.warn('Could not load storage pools:', error);
}
// Load VM templates
try {
const response = await fetch('/api/vms/templates');
if (response.ok) {
const data = await response.json();
const templateSelect = document.getElementById('vm-template');
if (templateSelect && data.templates) {
templateSelect.innerHTML = '<option value="">Select template...</option>';
data.templates.forEach(template => {
const option = document.createElement('option');
option.value = template.id;
option.textContent = template.name;
templateSelect.appendChild(option);
});
}
}
} catch (error) {
console.warn('Could not load VM templates:', error);
const templateSelect = document.getElementById('vm-template');
if (templateSelect) {
templateSelect.innerHTML = '<option value="">No templates available</option>';
}
}
},
getTotalStorageUsed() {
return this.storagePools.reduce((total, pool) => total + (pool.used || 0), 0);
},
async managePool(id) {
// TODO: Implement pool management modal
this.showNotification('Pool management feature coming soon', 'info');
},
async scrubPool(id) {
try {
if (window.PersistenceStorage) {
await window.PersistenceStorage.scrubPool(id);
this.showNotification('Pool scrub started successfully', 'success');
}
} catch (error) {
console.error('Failed to start pool scrub:', error);
this.showNotification('Failed to start pool scrub: ' + error.message, 'error');
}
},
async editDataset(id) {
// TODO: Implement dataset editing modal
this.showNotification('Dataset editing feature coming soon', 'info');
},
async snapshotDataset(id) {
try {
if (window.PersistenceSnapshots) {
await window.PersistenceSnapshots.createSnapshot(`dataset-${id}-${Date.now()}`);
this.showNotification('Dataset snapshot created successfully', 'success');
}
} catch (error) {
console.error('Failed to create dataset snapshot:', error);
this.showNotification('Failed to create dataset snapshot: ' + error.message, 'error');
}
},
showCreateDatasetModal() {
console.log('đ Opening Create Dataset Modal');
// Create modal HTML using the working pattern from config.sh
const modalHtml = `
<div id="createDatasetModal" class="modal-overlay" style="display: flex;">
<div class="modal-content" style="max-width: 600px; width: 90%; max-height: 90vh; overflow-y: auto;">
<div class="modal-header">
<h3><i class="fas fa-database"></i> Create Dataset</h3>
<button class="modal-close" onclick="closeCreateDatasetModal()">×</button>
</div>
<div class="modal-body">
<form id="createDatasetForm">
<div class="form-group">
<label for="dataset-name">Dataset Name *</label>
<input type="text" id="dataset-name" name="name"
placeholder="e.g., data, backups, media" required>
<small class="form-help">Choose a descriptive name for your dataset</small>
</div>
<div class="form-group">
<label for="dataset-pool">Storage Pool *</label>
<select id="dataset-pool" name="pool" required>
<option value="">Select a storage pool...</option>
</select>
<small class="form-help">Select the storage pool where this dataset will be created</small>
</div>
<div class="form-group">
<label for="dataset-mountpoint">Mount Point</label>
<input type="text" id="dataset-mountpoint" name="mountpoint"
placeholder="e.g., /mnt/data, /home/shared">
<small class="form-help">Optional: Specify where to mount this dataset</small>
</div>
<div class="form-group">
<label for="dataset-quota">Quota (Optional)</label>
<input type="text" id="dataset-quota" name="quota"
placeholder="e.g., 100G, 1T">
<small class="form-help">Set a size limit for this dataset (leave empty for no limit)</small>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="dataset-compression" name="compression" checked>
Enable Compression
</label>
<small class="form-help">Compress data to save space (recommended for most use cases)</small>
</div>
<div class="form-group">
<label for="dataset-description">Description</label>
<textarea id="dataset-description" name="description"
placeholder="Optional description of this dataset's purpose"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeCreateDatasetModal()">Cancel</button>
<button type="button" class="btn btn-primary" onclick="createDatasetFromModal()">
<i class="fas fa-plus"></i> Create Dataset
</button>
</div>
</div>
</div>
`;
// Add modal to DOM
document.body.insertAdjacentHTML('beforeend', modalHtml);
document.body.classList.add('modal-open');
// Populate storage pools dropdown
this.populateStoragePoolsDropdown();
// Focus on name field
setTimeout(() => {
document.getElementById('dataset-name').focus();
}, 100);
},
populateStoragePoolsDropdown() {
const poolSelect = document.getElementById('dataset-pool');
if (poolSelect && this.storagePools) {
// Clear existing options except the first one
poolSelect.innerHTML = '<option value="">Select a storage pool...</option>';
// Add pool options
this.storagePools.forEach(pool => {
const option = document.createElement('option');
option.value = pool.name;
option.textContent = `${pool.name} (${pool.type}) - ${pool.available || 'Unknown'} available`;
poolSelect.appendChild(option);
});
}
},
async createDataset() {
try {
// Validate required fields
if (!this.newDataset.name || !this.newDataset.pool) {
this.showNotification('Please fill in all required fields', 'error');
return;
}
// Prepare dataset data
const datasetData = {
name: this.newDataset.name,
pool: this.newDataset.pool,
mountpoint: this.newDataset.mountpoint,
quota: this.newDataset.quota,
compression: this.newDataset.compression,
description: this.newDataset.description,
type: 'filesystem'
};
this.showNotification(`Creating dataset "${this.newDataset.name}"...`, 'info');
// Call backend API to create dataset
const response = await fetch('/api/storage/datasets/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(datasetData)
});
if (response.ok) {
const result = await response.json();
this.showNotification(`Dataset "${this.newDataset.name}" created successfully`, 'success');
// Reset form
this.newDataset = {
name: '',
pool: '',
mountpoint: '',
quota: '',
compression: true,
description: ''
};
// Close modal
this.showCreateDatasetModal = false;
// Refresh storage data
await this.refreshStorageData();
} else {
const error = await response.json();
this.showNotification(`Failed to create dataset: ${error.detail || 'Unknown error'}`, 'error');
}
} catch (error) {
console.error('Failed to create dataset:', error);
this.showNotification('Failed to create dataset: ' + error.message, 'error');
}
},
async deleteDataset(id) {
if (confirm('Are you sure you want to delete this dataset? This action cannot be undone.')) {
try {
if (window.PersistenceStorage) {
await window.PersistenceStorage.deleteDataset(id);
await this.refreshStorageData();
this.showNotification('Dataset deleted successfully', 'success');
}
} catch (error) {
console.error('Failed to delete dataset:', error);
this.showNotification('Failed to delete dataset: ' + error.message, 'error');
}
}
},
// Network methods - using v2 modules
async refreshNetworkData() {
try {
if (window.PersistenceNetwork) {
await window.PersistenceNetwork.refreshNetwork();
this.networkInterfaces = window.PersistenceNetwork.getInterfaces();
this.dnsServers = window.PersistenceNetwork.getDNSServers();
this.firewallStatus = window.PersistenceNetwork.getFirewallStatus();
}
} catch (error) {
console.error('Failed to refresh network data:', error);
}
},
getActiveInterfaces() {
return this.networkInterfaces.filter(iface => iface.status === 'up').length;
},
getTotalNetworkTraffic() {
return this.networkInterfaces.reduce((total, iface) => total + (iface.rx || 0) + (iface.tx || 0), 0);
},
getInterfaceIcon(type) {
switch (type) {
case 'ethernet': return 'fa-ethernet';
case 'wifi': return 'fa-wifi';
case 'bridge': return 'fa-project-diagram';
case 'virtual': return 'fa-sitemap';
default: return 'fa-network-wired';
}
},
async configureInterface(id) {
// TODO: Implement interface configuration modal
this.showNotification('Interface configuration feature coming soon', 'info');
},
async toggleInterface(id) {
try {
if (window.PersistenceNetwork) {
await window.PersistenceNetwork.toggleInterface(id);
await this.refreshNetworkData();
this.showNotification('Interface toggled successfully', 'success');
}
} catch (error) {
console.error('Failed to toggle interface:', error);
this.showNotification('Failed to toggle interface: ' + error.message, 'error');
}
},
async resetInterface(id) {
if (confirm('Are you sure you want to reset this interface?')) {
try {
if (window.PersistenceNetwork) {
await window.PersistenceNetwork.resetInterface(id);
await this.refreshNetworkData();
this.showNotification('Interface reset successfully', 'success');
}
} catch (error) {
console.error('Failed to reset interface:', error);
this.showNotification('Failed to reset interface: ' + error.message, 'error');
}
}
},
getDNSStatus(dns) {
// Simple DNS status check (placeholder)
return Math.random() > 0.2 ? 'online' : 'offline';
},
getDNSStatusText(dns) {
return this.getDNSStatus(dns) === 'online' ? 'Online' : 'Offline';
},
async toggleFirewall() {
try {
if (window.PersistenceNetwork) {
await window.PersistenceNetwork.toggleFirewall();
await this.refreshNetworkData();
this.showNotification('Firewall toggled successfully', 'success');
}
} catch (error) {
console.error('Failed to toggle firewall:', error);
this.showNotification('Failed to toggle firewall: ' + error.message, 'error');
}
},
showFirewallRules() {
// TODO: Implement firewall rules modal
this.showNotification('Firewall rules viewer coming soon', 'info');
},
getDefaultGateway() {
// TODO: Get from network data
return '192.168.1.1';
},
getStaticRoutesCount() {
// TODO: Get from network data
return 0;
},
getDefaultMetric() {
// TODO: Get from network data
return 100;
},
// Settings methods
getBootTime() {
const bootTime = new Date(Date.now() - (this.systemStats.uptime || 0) * 1000);
return bootTime.toLocaleString();
},
getLastUpdateTime() {
// TODO: Get from system data
return 'Never';
},
showGeneralSettings() {
this.showNotification('General settings configuration coming soon', 'info');
},
showUserManagement() {
this.showNotification('User management interface coming soon', 'info');
},
showSecuritySettings() {
this.showNotification('Security settings configuration coming soon', 'info');
},
async checkSystemUpdates() {
try {
this.showNotification('Checking for system updates...', 'info');
// TODO: Implement actual update check using transactional-update
setTimeout(() => {
this.showNotification('System is up to date', 'success');
}, 2000);
} catch (error) {
console.error('Failed to check updates:', error);
this.showNotification('Failed to check for updates: ' + error.message, 'error');
}
},
showDateTimeSettings() {
this.showNotification('Date & time settings coming soon', 'info');
},
showMonitoringSettings() {
this.showNotification('Monitoring configuration coming soon', 'info');
},
showSystemServices() {
this.showNotification('System services viewer coming soon', 'info');
},
showSystemLogs() {
this.showNotification('System logs viewer coming soon', 'info');
},
showBackupSettings() {
this.showNotification('Backup & restore configuration coming soon', 'info');
},
async restartSystem() {
if (confirm('Are you sure you want to restart the system? This will interrupt all running services.')) {
try {
this.showNotification('System restart initiated...', 'warning');
// TODO: Implement actual system restart
} catch (error) {
console.error('Failed to restart system:', error);
this.showNotification('Failed to restart system: ' + error.message, 'error');
}
}
},
async shutdownSystem() {
if (confirm('Are you sure you want to shutdown the system? This will power off the machine.')) {
try {
this.showNotification('System shutdown initiated...', 'warning');
// TODO: Implement actual system shutdown
} catch (error) {
console.error('Failed to shutdown system:', error);
this.showNotification('Failed to shutdown system: ' + error.message, 'error');
}
}
},
// Snapshots methods - using v2 modules
async refreshSnapshotData() {
try {
if (window.PersistenceSnapshots) {
await window.PersistenceSnapshots.refreshSnapshots();
this.snapshots = window.PersistenceSnapshots.getSnapshots();
this.snapshotStats = window.PersistenceSnapshots.getSnapshotStats();
this.snapshotDataSource = window.PersistenceSnapshots.data?.source || 'snapper';
}
} catch (error) {
console.error('Failed to refresh snapshot data:', error);
}
},
async createSnapshot() {
try {
if (window.PersistenceSnapshots) {
await window.PersistenceSnapshots.createSnapshot('manual-' + Date.now());
await this.refreshSnapshotData();
this.showNotification('Snapshot created successfully', 'success');
}
} catch (error) {
console.error('Failed to create snapshot:', error);
this.showNotification('Failed to create snapshot: ' + error.message, 'error');
}
},
async createSystemSnapshot() {
try {
if (window.PersistenceSnapshots) {
await window.PersistenceSnapshots.createSnapshot('system-' + Date.now());
await this.refreshSnapshotData();
this.showNotification('System snapshot created successfully', 'success');
}
} catch (error) {
console.error('Failed to create system snapshot:', error);
this.showNotification('Failed to create system snapshot: ' + error.message, 'error');
}
},
async restoreSnapshot(id) {
if (confirm('Are you sure you want to restore this snapshot? This will revert the system to the snapshot state.')) {
try {
if (window.PersistenceSnapshots) {
await window.PersistenceSnapshots.restoreSnapshot(id);
this.showNotification('Snapshot restore initiated successfully', 'success');
}
} catch (error) {
console.error('Failed to restore snapshot:', error);
this.showNotification('Failed to restore snapshot: ' + error.message, 'error');
}
}
},
async cloneSnapshot(id) {
// TODO: Implement snapshot cloning
this.showNotification('Snapshot cloning feature coming soon', 'info');
},
async exportSnapshot(id) {
// TODO: Implement snapshot export
this.showNotification('Snapshot export feature coming soon', 'info');
},
async deleteSnapshot(id) {
if (confirm('Are you sure you want to delete this snapshot? This action cannot be undone.')) {
try {
if (window.PersistenceSnapshots) {
await window.PersistenceSnapshots.deleteSnapshot(id);
await this.refreshSnapshotData();
this.showNotification('Snapshot deleted successfully', 'success');
}
} catch (error) {
console.error('Failed to delete snapshot:', error);
this.showNotification('Failed to delete snapshot: ' + error.message, 'error');
}
}
},
formatSnapshotDate(dateString) {
if (!dateString) return 'Unknown';
const date = new Date(dateString);
return date.toLocaleString();
},
getNextSnapshotTime(type) {
const now = new Date();
switch (type) {
case 'hourly':
const nextHour = new Date(now);
nextHour.setHours(now.getHours() + 1, 0, 0, 0);
return nextHour.toLocaleTimeString();
case 'daily':
const nextDay = new Date(now);
nextDay.setDate(now.getDate() + 1);
nextDay.setHours(2, 0, 0, 0);
return nextDay.toLocaleString();
default:
return 'Not scheduled';
}
},
// Utility methods
async refreshSectionData(section) {
switch (section) {
case 'dashboard':
await this.refreshSystemInfo();
break;
case 'vms':
await this.refreshVMData();
break;
case 'storage':
await this.refreshStorageData();
break;
case 'network':
await this.refreshNetworkData();
break;
case 'snapshots':
await this.refreshSnapshotData();
break;
}
},
formatBytes(bytes) {
if (window.formatBytes) {
return window.formatBytes(bytes);
}
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
formatUptime(seconds) {
if (window.formatUptime) {
return window.formatUptime(seconds);
}
if (!seconds) return 'Online';
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (days > 0) {
return `${days} days, ${hours} hours`;
} else if (hours > 0) {
return `${hours} hours, ${minutes} minutes`;
} else {
return `${minutes} minutes`;
}
},
getUsageClass(usage) {
if (usage < 50) return 'low';
if (usage < 80) return 'medium';
return 'high';
},
showNotification(message, type = 'info') {
if (window.showNotification) {
window.showNotification(message, type);
} else {
// Log notification if notification system not available
console.log(`${type.toUpperCase()}: ${message}`);
}
},
async logout() {
try {
if (window.PersistenceAuthV2) {
await window.PersistenceAuthV2.logout();
}
window.location.href = '/login.html';
} catch (error) {
console.error('Logout failed:', error);
window.location.href = '/login.html';
}
}
},
async mounted() {
console.log('đ PersistenceOS v2 Dashboard mounted successfully');
// Wait for v2 modules to be ready
await new Promise(resolve => setTimeout(resolve, 1000));
// Initialize data
await this.refreshSystemInfo();
await this.refreshVMData();
await this.refreshStorageData();
await this.refreshNetworkData();
await this.refreshSnapshotData();
// Set up auto-refresh for current section
setInterval(() => {
this.refreshSectionData(this.activeSection);
}, 30000); // Refresh every 30 seconds
console.log('â
All v2 modules connected and data loaded');
// Listen for dashboard data updates
window.addEventListener('dashboardDataUpdate', (event) => {
if (event.detail && event.detail.systemStats) {
this.systemStats = event.detail.systemStats;
console.log('đ Dashboard data updated from v2 module');
}
});
}
});
// Mount the application
PersistenceOSApp.mount('#app');
// Global modal functions (following the working pattern from config.sh)
window.closeCreateDatasetModal = () => {
const modal = document.getElementById('createDatasetModal');
if (modal) {
modal.remove();
document.body.classList.remove('modal-open');
}
};
window.createDatasetFromModal = async () => {
try {
const form = document.getElementById('createDatasetForm');
const formData = new FormData(form);
const datasetData = {
name: formData.get('name'),
pool: formData.get('pool'),
mountpoint: formData.get('mountpoint') || '',
quota: formData.get('quota') || '',
compression: formData.get('compression') === 'on',
description: formData.get('description') || '',
type: 'filesystem'
};
// Validate required fields
if (!datasetData.name || !datasetData.pool) {
alert('Please fill in all required fields');
return;
}
// Show loading state
const createBtn = document.querySelector('#createDatasetModal .btn-primary');
const originalText = createBtn.innerHTML;
createBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating...';
createBtn.disabled = true;
// Call backend API
const response = await fetch('/api/storage/datasets/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(datasetData)
});
if (response.ok) {
const result = await response.json();
// Show success notification
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(`Dataset "${datasetData.name}" created successfully`, 'success');
} else {
alert(`Dataset "${datasetData.name}" created successfully`);
}
// Close modal
closeCreateDatasetModal();
// Refresh storage data
if (window.app && typeof window.app.refreshStorageData === 'function') {
await window.app.refreshStorageData();
}
} else {
const error = await response.json();
throw new Error(error.detail || 'Failed to create dataset');
}
} catch (error) {
console.error('Failed to create dataset:', error);
// Show error notification
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification('Failed to create dataset: ' + error.message, 'error');
} else {
alert('Failed to create dataset: ' + error.message);
}
// Reset button
const createBtn = document.querySelector('#createDatasetModal .btn-primary');
if (createBtn) {
createBtn.innerHTML = '<i class="fas fa-plus"></i> Create Dataset';
createBtn.disabled = false;
}
}
};
// Global Create Pool Modal Functions
window.closeCreatePoolModal = () => {
const modal = document.getElementById('createPoolModal');
if (modal) {
modal.remove();
document.body.classList.remove('modal-open');
}
};
// Global variable to store available devices
window.availableDevices = [];
// Refresh device list from backend
window.refreshDeviceList = async () => {
try {
const response = await fetch('/api/storage/devices');
if (response.ok) {
const data = await response.json();
window.availableDevices = data.devices || [];
// Update all device dropdowns
const deviceSelects = document.querySelectorAll('.device-select');
deviceSelects.forEach(select => {
populateDeviceDropdown(select.id);
});
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(`Found ${window.availableDevices.length} available storage devices`, 'success');
}
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
console.error('Failed to refresh device list:', error);
// Clear devices and show error - no mock data fallback
window.availableDevices = [];
// Update all device dropdowns to show error state
const deviceSelects = document.querySelectorAll('.device-select');
deviceSelects.forEach(select => {
select.innerHTML = '<option value="">No devices available - Check API connection</option>';
});
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(`Failed to load storage devices: ${error.message}`, 'error');
} else {
console.error('Failed to load storage devices:', error.message);
}
}
};
// Add device selection dropdown
window.addDeviceInput = () => {
const deviceArea = document.getElementById('device-selection-area');
const currentInputs = deviceArea.querySelectorAll('.device-input-group');
const deviceNumber = currentInputs.length + 1;
const deviceInputHtml = `
<div class="device-input-group" style="margin-top: 0.5rem;">
<select id="pool-device-${deviceNumber}" name="devices[]" class="form-control device-select" required>
<option value="">Select device ${deviceNumber}...</option>
<option value="loading">Loading available devices...</option>
</select>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeDeviceInput(this)" style="margin-left: 0.5rem;">
<i class="fas fa-times"></i>
</button>
</div>
`;
deviceArea.insertAdjacentHTML('beforeend', deviceInputHtml);
// Populate the new dropdown with available devices
populateDeviceDropdown(`pool-device-${deviceNumber}`);
};
// Remove device input field
window.removeDeviceInput = (button) => {
const deviceGroup = button.closest('.device-input-group');
const deviceArea = document.getElementById('device-selection-area');
const currentInputs = deviceArea.querySelectorAll('.device-input-group');
// Don't remove if it's the last input
if (currentInputs.length > 1) {
deviceGroup.remove();
}
};
// Populate a specific device dropdown
function populateDeviceDropdown(selectId) {
const select = document.getElementById(selectId);
if (!select) return;
const currentValue = select.value;
const deviceNumber = selectId.split('-').pop();
const isFirst = deviceNumber === '1';
// Clear existing options
select.innerHTML = '';
// Add default option
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = isFirst ? 'Select primary device...' : `Select device ${deviceNumber}...`;
select.appendChild(defaultOption);
// Check if we have devices
if (!window.availableDevices || window.availableDevices.length === 0) {
const noDevicesOption = document.createElement('option');
noDevicesOption.value = '';
noDevicesOption.textContent = 'No available devices found';
noDevicesOption.disabled = true;
select.appendChild(noDevicesOption);
return;
}
// Get already selected devices to avoid duplicates
const selectedDevices = Array.from(document.querySelectorAll('.device-select'))
.map(s => s.value)
.filter(v => v && v !== 'loading' && v !== '');
// Add available devices (using real backend data format)
window.availableDevices.forEach(device => {
// Skip if device is already selected in another dropdown
if (selectedDevices.includes(device.path) && currentValue !== device.path) {
return;
}
const option = document.createElement('option');
option.value = device.path;
// Format device display text using real backend data
// Backend provides: path, name, size, type, available
let displayText = `${device.path} - ${device.size}`;
// Add device type if available
if (device.type) {
const deviceType = device.type.toUpperCase();
displayText += ` (${deviceType})`;
// Style based on device type
if (deviceType === 'DISK') {
// Check if it's likely an SSD or NVMe based on path
if (device.path.includes('nvme')) {
option.style.fontWeight = 'bold';
option.style.color = '#28a745'; // Green for NVMe
displayText = displayText.replace('(DISK)', '(NVMe)');
} else {
option.style.color = '#0066cc'; // Blue for regular disks
}
}
}
// Mark unavailable devices
if (device.available === false) {
displayText += ' [IN USE]';
option.disabled = true;
option.style.color = '#dc3545'; // Red for unavailable
}
option.textContent = displayText;
select.appendChild(option);
});
// Restore previous selection if valid
if (currentValue && currentValue !== 'loading') {
select.value = currentValue;
}
}
// Legacy function for backward compatibility
window.detectDevices = async () => {
await window.refreshDeviceList();
};
// Initialize device list when modal opens
window.initializeDeviceSelection = async () => {
await window.refreshDeviceList();
};
window.updatePoolTypeOptions = () => {
const poolTypeElement = document.getElementById('pool-type');
if (!poolTypeElement) {
console.warn('â ī¸ Pool type element not found');
return;
}
const storageType = poolTypeElement.value;
const advancedOptions = document.getElementById('advanced-options');
const btrfsOptions = document.getElementById('btrfs-options');
const raidOptions = document.getElementById('raid-options');
const filesystemOptions = document.getElementById('filesystem-options');
const performanceOptions = document.getElementById('performance-options');
const raidInfo = document.getElementById('raid-info');
const addDeviceBtn = document.getElementById('add-device-btn');
const deviceHelpText = document.getElementById('device-help-text');
// Check if required elements exist
if (!advancedOptions || !raidInfo || !deviceHelpText) {
console.warn('â ī¸ Required DOM elements not found for pool type options');
return;
}
// RAID configuration data
const raidConfigs = {
'single-btrfs': { devices: 1, description: 'Single disk with BTRFS filesystem', requirements: 'Minimum: 1 device' },
'single-xfs': { devices: 1, description: 'Single disk with XFS filesystem', requirements: 'Minimum: 1 device' },
'single-ext4': { devices: 1, description: 'Single disk with EXT4 filesystem', requirements: 'Minimum: 1 device' },
'btrfs-raid0': { devices: 2, description: 'BTRFS RAID 0 - Data striped across devices for maximum performance', requirements: 'Minimum: 2 devices, No redundancy' },
'btrfs-raid1': { devices: 2, description: 'BTRFS RAID 1 - Data mirrored across devices for redundancy', requirements: 'Minimum: 2 devices, 50% usable space' },
'btrfs-raid10': { devices: 4, description: 'BTRFS RAID 10 - Striped mirrors for performance and redundancy', requirements: 'Minimum: 4 devices, 50% usable space' },
'mdadm-raid0': { devices: 2, description: 'Software RAID 0 - Maximum performance, no redundancy', requirements: 'Minimum: 2 devices, No fault tolerance' },
'mdadm-raid1': { devices: 2, description: 'Software RAID 1 - Complete data mirroring', requirements: 'Minimum: 2 devices, 50% usable space' },
'mdadm-raid5': { devices: 3, description: 'Software RAID 5 - Distributed parity, good balance', requirements: 'Minimum: 3 devices, 1 drive fault tolerance' },
'mdadm-raid6': { devices: 4, description: 'Software RAID 6 - Dual parity, high protection', requirements: 'Minimum: 4 devices, 2 drive fault tolerance' },
'mdadm-raid10': { devices: 4, description: 'Software RAID 10 - Striped mirrors, enterprise grade', requirements: 'Minimum: 4 devices, 50% usable space' },
'lvm-stripe': { devices: 2, description: 'LVM striped logical volume for performance', requirements: 'Minimum: 2 devices, No redundancy' },
'lvm-mirror': { devices: 2, description: 'LVM mirrored logical volume for redundancy', requirements: 'Minimum: 2 devices, 50% usable space' }
};
if (storageType && raidConfigs[storageType]) {
advancedOptions.style.display = 'block';
const config = raidConfigs[storageType];
// Update device requirements
if (typeof updateDeviceInputs === 'function') {
updateDeviceInputs(config.devices);
}
deviceHelpText.textContent = config.requirements;
// Show RAID info
raidInfo.style.display = 'block';
const raidDescription = document.getElementById('raid-description');
const raidRequirements = document.getElementById('raid-requirements');
if (raidDescription) raidDescription.textContent = config.description;
if (raidRequirements) raidRequirements.innerHTML = `<strong>Requirements:</strong> ${config.requirements}`;
// Show/hide option sections based on storage type
if (storageType.includes('btrfs')) {
if (btrfsOptions) btrfsOptions.style.display = 'block';
if (raidOptions) raidOptions.style.display = 'none';
if (filesystemOptions) filesystemOptions.style.display = 'none';
} else if (storageType.includes('mdadm')) {
if (btrfsOptions) btrfsOptions.style.display = 'none';
if (raidOptions) raidOptions.style.display = 'block';
if (filesystemOptions) filesystemOptions.style.display = 'block';
} else if (storageType.includes('lvm')) {
if (btrfsOptions) btrfsOptions.style.display = 'none';
if (raidOptions) raidOptions.style.display = 'none';
if (filesystemOptions) filesystemOptions.style.display = 'block';
} else {
if (btrfsOptions) btrfsOptions.style.display = 'none';
if (raidOptions) raidOptions.style.display = 'none';
if (filesystemOptions) filesystemOptions.style.display = 'none';
}
// Show performance options for RAID configurations
if (storageType.includes('raid') || storageType.includes('stripe') || storageType.includes('mirror')) {
performanceOptions.style.display = 'block';
} else {
performanceOptions.style.display = 'none';
}
} else {
advancedOptions.style.display = 'none';
raidInfo.style.display = 'none';
addDeviceBtn.style.display = 'none';
deviceHelpText.textContent = 'Single device required. Additional devices will be shown based on RAID selection.';
}
};
// Helper function to update device input fields
function updateDeviceInputs(requiredDevices) {
const deviceArea = document.getElementById('device-selection-area');
const addBtn = document.getElementById('add-device-btn');
const currentInputs = deviceArea.querySelectorAll('.device-input-group');
// Show add button if more than 1 device needed
if (requiredDevices > 1) {
addBtn.style.display = 'inline-block';
// Add device inputs up to required amount
for (let i = currentInputs.length; i < requiredDevices; i++) {
addDeviceInput();
}
} else {
addBtn.style.display = 'none';
// Remove extra device inputs, keep only first one
for (let i = currentInputs.length - 1; i > 0; i--) {
currentInputs[i].remove();
}
}
}
window.detectDevices = async () => {
try {
const response = await fetch('/api/storage/devices');
const data = await response.json();
if (response.ok && data.devices && data.devices.length > 0) {
let deviceList = 'Available devices:\n\n';
data.devices.forEach(device => {
const sizeGB = device.size ? Math.round(device.size / (1024*1024*1024)) : 'Unknown';
deviceList += `${device.path} - ${device.model || 'Unknown'} (${sizeGB}GB)\n`;
});
const selectedDevice = prompt(deviceList + '\nEnter the device path you want to use:');
if (selectedDevice) {
document.getElementById('pool-device').value = selectedDevice;
}
} else {
alert('No storage devices found or device detection not available in test environment.');
}
} catch (error) {
console.error('Device detection failed:', error);
alert('Device detection failed. Please enter the device path manually.');
}
};
window.createPoolFromModal = async () => {
try {
const form = document.getElementById('createPoolForm');
if (!form) {
console.error('â Create pool form not found');
return;
}
const formData = new FormData(form);
// Get available devices to check sizes
const devicesResponse = await fetch('/api/storage/devices');
const devicesData = await devicesResponse.json();
const selectedDevicePath = formData.getAll('devices[]').filter(device => device && device !== '')[0];
const selectedDevice = devicesData.devices.find(d => d.path === selectedDevicePath);
console.log('đ Selected device:', selectedDevice);
// Check device size and auto-select appropriate filesystem
let recommendedFS = 'btrfs';
let storageType = formData.get('storage_type');
if (selectedDevice) {
const sizeStr = selectedDevice.size || '0';
const sizeMatch = sizeStr.match(/(\d+(?:\.\d+)?)\s*(GB|MB|TB)/i);
if (sizeMatch) {
const size = parseFloat(sizeMatch[1]);
const unit = sizeMatch[2].toUpperCase();
const sizeInMB = unit === 'GB' ? size * 1024 :
unit === 'TB' ? size * 1024 * 1024 : size;
if (sizeInMB < 115) {
console.log('â ī¸ Device too small for BTRFS, using XFS instead');
recommendedFS = 'xfs';
// Override storage type for small devices
if (storageType === 'single-btrfs') {
storageType = 'single-xfs';
}
}
}
}
// Map frontend data to backend format
const storageTypeMap = {
'single-btrfs': 'btrfs',
'single-xfs': 'xfs',
'single-ext4': 'ext4',
'btrfs-raid0': 'btrfs',
'btrfs-raid1': 'btrfs'
};
// Use backend-compatible format
const poolData = {
name: formData.get('name'),
filesystem_type: storageTypeMap[storageType] || recommendedFS,
device_path: selectedDevicePath,
mount_point: `/mnt/${formData.get('name')}`,
size: null
};
console.log('đ Pool data to submit (with FS optimization):', poolData);
// Validate required fields
if (!poolData.name || !poolData.filesystem_type || !poolData.device_path) {
alert('Please fill in all required fields');
console.error('â Validation failed:', poolData);
return;
}
// Show loading state
const createBtn = document.querySelector('#createPoolModal .btn-primary');
if (createBtn) {
createBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating Pool...';
createBtn.disabled = true;
}
// Show notification with filesystem info
const fsInfo = poolData.filesystem_type === 'xfs' ? ' (using XFS for small device)' : '';
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(`Creating storage pool "${poolData.name}"${fsInfo}...`, 'info');
}
// Call backend API to create pool
const response = await fetch('/api/storage/pools/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(poolData)
});
if (response.ok) {
const result = await response.json();
// Show success notification
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(`â
Storage pool "${poolData.name}" created successfully with ${poolData.filesystem_type.toUpperCase()}!`, 'success');
} else {
alert(`Storage pool "${poolData.name}" created successfully!`);
}
// Close modal
window.closeCreatePoolModal();
// Refresh storage data
if (window.app && typeof window.app.refreshStorageData === 'function') {
await window.app.refreshStorageData();
}
} else {
const error = await response.json();
throw new Error(error.message || 'Failed to create storage pool');
}
} catch (error) {
console.error('Failed to create storage pool:', error);
// Show error notification
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification('Failed to create storage pool: ' + error.message, 'error');
} else {
alert('Failed to create storage pool: ' + error.message);
}
// Reset button
const createBtn = document.querySelector('#createPoolModal .btn-primary');
if (createBtn) {
createBtn.innerHTML = '<i class="fas fa-plus"></i> Create Pool';
createBtn.disabled = false;
}
}
};
// Global VM Console Functions
window.closeVMConsole = () => {
const modal = document.getElementById('vmConsoleModal');
if (modal) {
modal.remove();
document.body.classList.remove('modal-open');
}
};
window.toggleConsoleSize = () => {
const modal = document.getElementById('vmConsoleModal');
const modalContent = modal?.querySelector('.modal-content');
if (modalContent) {
if (modalContent.classList.contains('console-expanded')) {
// Shrink to normal size
modalContent.classList.remove('console-expanded');
modalContent.style.maxWidth = '1200px';
modalContent.style.width = '95%';
modalContent.style.maxHeight = '95vh';
} else {
// Expand to larger size
modalContent.classList.add('console-expanded');
modalContent.style.maxWidth = '98vw';
modalContent.style.width = '98vw';
modalContent.style.maxHeight = '98vh';
}
}
};
window.refreshVMConsole = async (vmId) => {
const consoleStatus = document.getElementById('consoleStatus');
const consoleConnection = document.getElementById('consoleConnection');
if (consoleStatus) consoleStatus.textContent = 'Refreshing...';
if (consoleConnection) consoleConnection.textContent = 'Reconnecting...';
try {
// Re-initialize console connection
if (window.app && typeof window.app.initializeVMConsole === 'function') {
await window.app.initializeVMConsole(vmId, 'VM');
} else {
// Fallback refresh
setTimeout(() => {
if (consoleStatus) consoleStatus.textContent = 'Available';
if (consoleConnection) consoleConnection.textContent = 'Connected';
}, 1000);
}
} catch (error) {
console.error('Failed to refresh console:', error);
if (consoleStatus) consoleStatus.textContent = 'Error';
if (consoleConnection) consoleConnection.textContent = 'Failed';
}
};
window.sendCtrlAltDel = async (vmId) => {
try {
const response = await fetch(`/api/vms/${vmId}/send-key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key_combination: 'ctrl-alt-del' })
});
if (response.ok) {
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification('Ctrl+Alt+Del sent to VM', 'success');
}
} else {
throw new Error('Failed to send key combination');
}
} catch (error) {
console.error('Failed to send Ctrl+Alt+Del:', error);
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification('Failed to send Ctrl+Alt+Del', 'error');
}
}
};
// Enhanced Console Functions
window.sendSpecialKey = async (vmId, keyCombo) => {
try {
const response = await fetch(`/api/vms/${vmId}/send-key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ key_combination: keyCombo })
});
if (response.ok) {
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(`${keyCombo.toUpperCase()} sent to VM`, 'success');
}
} else {
throw new Error('Failed to send key combination');
}
} catch (error) {
console.error(`Failed to send ${keyCombo}:`, error);
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(`Failed to send ${keyCombo}`, 'error');
}
}
};
window.takeVMScreenshot = async (vmId) => {
try {
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification('Taking VM screenshot...', 'info');
}
const response = await fetch(`/api/vms/${vmId}/screenshot`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
const result = await response.json();
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(`Screenshot saved: ${result.filename}`, 'success');
}
} else {
throw new Error('Failed to take screenshot');
}
} catch (error) {
console.error('Failed to take screenshot:', error);
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification('Screenshot feature in development', 'info');
}
}
};
window.showVMPerformance = (vmId) => {
const sidebar = document.getElementById('vmPerformanceSidebar');
if (sidebar) {
const isVisible = sidebar.style.display !== 'none';
sidebar.style.display = isVisible ? 'none' : 'block';
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(
isVisible ? 'Performance monitor hidden' : 'Performance monitor shown',
'info'
);
}
}
};
window.createVMSnapshot = async (vmId) => {
try {
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification('Creating VM snapshot...', 'info');
}
const response = await fetch(`/api/vms/${vmId}/snapshot`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: `snapshot-${Date.now()}`,
description: 'Console snapshot'
})
});
if (response.ok) {
const result = await response.json();
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(`Snapshot created: ${result.snapshot_name}`, 'success');
}
} else {
throw new Error('Failed to create snapshot');
}
} catch (error) {
console.error('Failed to create snapshot:', error);
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification('Failed to create snapshot', 'error');
}
}
};
// Global Create VM Modal Functions
window.closeCreateVMModal = () => {
const modal = document.getElementById('createVMModal');
if (modal) {
modal.remove();
document.body.classList.remove('modal-open');
}
};
window.updateOSDefaults = () => {
const osType = document.getElementById('vm-os-type').value;
const cpuInput = document.getElementById('vm-cpu-cores');
const memoryInput = document.getElementById('vm-memory');
const diskInput = document.getElementById('vm-disk-size');
const firmwareSelect = document.getElementById('vm-boot-firmware');
// Set defaults based on OS type
if (osType === 'windows') {
cpuInput.value = '2'; // 2 cores for Windows
memoryInput.value = '4'; // 4GB for Windows
diskInput.value = '80'; // 80GB for Windows
firmwareSelect.value = 'uefi';
} else if (osType === 'linux') {
cpuInput.value = '2'; // 2 cores for Linux
memoryInput.value = '2'; // 2GB for Linux
diskInput.value = '40'; // 40GB for Linux
firmwareSelect.value = 'uefi';
} else if (osType === 'other') {
cpuInput.value = '1'; // 1 core for other/custom
memoryInput.value = '1'; // 1GB for other/custom
diskInput.value = '20'; // 20GB for other/custom
firmwareSelect.value = 'bios';
}
};
window.updateInstallMethodOptions = () => {
const installMethod = document.getElementById('vm-install-method').value;
const isoUploadSection = document.getElementById('iso-upload-section');
const templateSection = document.getElementById('template-section');
// Hide all sections first
isoUploadSection.style.display = 'none';
templateSection.style.display = 'none';
// Show relevant sections
if (installMethod === 'iso-upload') {
isoUploadSection.style.display = 'block';
} else if (installMethod === 'template') {
templateSection.style.display = 'block';
}
};
window.createVMFromModal = async () => {
try {
const form = document.getElementById('createVMForm');
const formData = new FormData(form);
// Build VM configuration object
const vmConfig = {
name: formData.get('name'),
os_type: formData.get('os_type'),
cpu_cores: parseInt(formData.get('cpu_cores')),
memory_gb: parseInt(formData.get('memory_gb')),
disk_size_gb: parseInt(formData.get('disk_size_gb')),
install_method: formData.get('install_method'),
storage_pool: formData.get('storage_pool') || null,
boot_firmware: formData.get('boot_firmware') || 'uefi',
enable_virtio: formData.get('enable_virtio') === 'on',
start_after_creation: formData.get('start_after_creation') === 'on',
template_id: formData.get('template_id') || null
};
// Validate required fields
if (!vmConfig.name || !vmConfig.os_type || !vmConfig.install_method) {
alert('Please fill in all required fields');
return;
}
// Validate hardware configuration
if (vmConfig.cpu_cores < 1 || vmConfig.cpu_cores > 64) {
alert('CPU cores must be between 1 and 64');
return;
}
if (vmConfig.memory_gb < 0.5 || vmConfig.memory_gb > 256) {
alert('Memory must be between 0.5 GB and 256 GB');
return;
}
if (vmConfig.disk_size_gb < 10 || vmConfig.disk_size_gb > 10000) {
alert('Disk size must be between 10 GB and 10,000 GB (10 TB)');
return;
}
// Validate VM name (no spaces, special characters)
if (!/^[a-zA-Z0-9_-]+$/.test(vmConfig.name)) {
alert('VM name can only contain letters, numbers, hyphens, and underscores');
return;
}
// Show loading state
const createBtn = document.querySelector('#createVMModal .btn-primary');
if (createBtn) {
createBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating VM...';
createBtn.disabled = true;
}
// Show notification
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(`Creating virtual machine "${vmConfig.name}"...`, 'info');
}
// Handle ISO upload if needed
if (vmConfig.install_method === 'iso-upload') {
const isoFile = document.getElementById('vm-iso-file').files[0];
if (isoFile) {
// Upload ISO first
const uploadFormData = new FormData();
uploadFormData.append('file', isoFile);
uploadFormData.append('vm_name', vmConfig.name);
const uploadResponse = await fetch('/api/vms/upload-iso', {
method: 'POST',
body: uploadFormData
});
if (!uploadResponse.ok) {
const uploadError = await uploadResponse.json();
throw new Error(uploadError.detail || 'ISO upload failed');
}
const uploadResult = await uploadResponse.json();
vmConfig.iso_path = uploadResult.file_path;
}
}
// Create VM via API
const response = await fetch('/api/vms/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(vmConfig)
});
if (response.ok) {
const result = await response.json();
// Show success notification
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification(`â
Virtual machine "${vmConfig.name}" created successfully!`, 'success');
// Show additional info if available
if (result.note) {
setTimeout(() => {
window.app.showNotification(`âšī¸ ${result.note}`, 'info');
}, 2000);
}
} else {
alert(`Virtual machine "${vmConfig.name}" created successfully!`);
}
// Close modal
window.closeCreateVMModal();
// Refresh VM data
if (window.app && typeof window.app.refreshVMData === 'function') {
await window.app.refreshVMData();
}
} else {
const error = await response.json();
throw new Error(error.detail || error.message || 'VM creation failed');
}
} catch (error) {
console.error('Failed to create VM:', error);
// Show error notification
if (window.app && typeof window.app.showNotification === 'function') {
window.app.showNotification('Failed to create VM: ' + error.message, 'error');
} else {
alert('Failed to create VM: ' + error.message);
}
// Reset button
const createBtn = document.querySelector('#createVMModal .btn-primary');
if (createBtn) {
createBtn.innerHTML = '<i class="fas fa-plus"></i> Create VM';
createBtn.disabled = false;
}
}
};
</script>
</body>
</html>