feat: add Jinja2 templates for jobs management, login interface, and base layout styling

This commit is contained in:
Rizqi 2026-06-21 04:11:42 +07:00
parent 8ae38a42fb
commit 60422ac1a9
7 changed files with 212 additions and 83 deletions

View File

@ -111,7 +111,7 @@
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.05); box-shadow: 0 4px 12px rgba(99, 102, 241, 0.05);
font-weight: 600; font-weight: 600;
} }
.nav-link .icon { font-size: 18px; width: 22px; text-align: center; } .nav-link .icon { width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; }
.sidebar-footer { .sidebar-footer {
padding: 20px 24px; padding: 20px 24px;
@ -344,7 +344,14 @@
<aside class="sidebar"> <aside class="sidebar">
<div class="sidebar-logo"> <div class="sidebar-logo">
<div class="logo-mark"> <div class="logo-mark">
<div class="logo-icon">🛡</div> <div class="logo-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="color: #ffffff; display: block;">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
<path d="M8 11a4 4 0 0 1 6-3.46M16 13a4 4 0 0 1-6 3.46"/>
<path d="M12 6h2v2"/>
<path d="M12 18H10v-2"/>
</svg>
</div>
<div> <div>
<div class="logo-text">vSphere Backup</div> <div class="logo-text">vSphere Backup</div>
<div class="logo-sub">Manager</div> <div class="logo-sub">Manager</div>
@ -355,16 +362,28 @@
<nav class="sidebar-nav"> <nav class="sidebar-nav">
<div class="nav-section-label">Navigation</div> <div class="nav-section-label">Navigation</div>
<a href="/vms" class="nav-link {% if active_page == 'vms' %}active{% endif %}"> <a href="/vms" class="nav-link {% if active_page == 'vms' %}active{% endif %}">
<span class="icon">🖥</span> Virtual Machines <span class="icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
</span>
Virtual Machines
</a> </a>
<a href="/jobs" class="nav-link {% if active_page == 'jobs' %}active{% endif %}"> <a href="/jobs" class="nav-link {% if active_page == 'jobs' %}active{% endif %}">
<span class="icon">📋</span> Backup Jobs <span class="icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/></svg>
</span>
Backup Jobs
</a> </a>
<a href="/jobs/create" class="nav-link {% if active_page == 'create_job' %}active{% endif %}"> <a href="/jobs/create" class="nav-link {% if active_page == 'create_job' %}active{% endif %}">
<span class="icon"></span> Create Job <span class="icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
</span>
Create Job
</a> </a>
<a href="/nfs" class="nav-link {% if active_page == 'nfs' %}active{% endif %}"> <a href="/nfs" class="nav-link {% if active_page == 'nfs' %}active{% endif %}">
<span class="icon">📡</span> NFS Manager <span class="icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
</span>
NFS Manager
</a> </a>
</nav> </nav>
@ -374,13 +393,14 @@
<div class="server-user text-small">{{ session.get('user', '') }}</div> <div class="server-user text-small">{{ session.get('user', '') }}</div>
</div> </div>
<a href="/logout" class="btn btn-ghost btn-sm" style="width:100%;justify-content:center;margin-top:10px;"> <a href="/logout" class="btn btn-ghost btn-sm" style="width:100%;justify-content:center;margin-top:10px;">
⬅ Logout <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
Logout
</a> </a>
</div> </div>
</aside> </aside>
{% endif %} {% endif %}
<main class="main" {% if not session.get('host') %}style="margin-left:0"{% endif %}> <main class="main" {% if not session.get('host') %}style="margin-left:0; align-items:center; justify-content:center;"{% else %}style="min-height:100vh;"{% endif %}>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
<div style="padding:16px 32px 0"> <div style="padding:16px 32px 0">

View File

@ -131,7 +131,10 @@
<!-- VM Selection --> <!-- VM Selection -->
<div class="section-card"> <div class="section-card">
<div class="section-card-header">🖥 Virtual Machine</div> <div class="section-card-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
Virtual Machine
</div>
<div class="section-card-body"> <div class="section-card-body">
<div class="form-group" style="margin:0;"> <div class="form-group" style="margin:0;">
<label class="form-label" for="vm_name">Select VM to back up</label> <label class="form-label" for="vm_name">Select VM to back up</label>
@ -155,7 +158,8 @@
<input type="hidden" name="disk_selection_shown" id="disk_selection_shown" value="" /> <input type="hidden" name="disk_selection_shown" id="disk_selection_shown" value="" />
<div class="section-card" id="diskCard" style="display:none;"> <div class="section-card" id="diskCard" style="display:none;">
<div class="section-card-header"> <div class="section-card-header">
💾 Select Disks to Backup <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><rect x="2" y="2" width="20" height="20" rx="2" ry="2"/><path d="M12 18h.01"/><path d="M8 6h8"/><path d="M8 10h8"/></svg>
Select Disks to Backup
<span id="diskCardBadge" style="margin-left:auto;font-size:12px;color:var(--text-muted);font-weight:500;"></span> <span id="diskCardBadge" style="margin-left:auto;font-size:12px;color:var(--text-muted);font-weight:500;"></span>
</div> </div>
<div class="section-card-body"> <div class="section-card-body">
@ -166,7 +170,8 @@
<div id="diskTip" style="display:none;margin-top:14px;padding:12px 16px; <div id="diskTip" style="display:none;margin-top:14px;padding:12px 16px;
background:rgba(6,182,212,0.06);border:1px solid rgba(6,182,212,0.15); background:rgba(6,182,212,0.06);border:1px solid rgba(6,182,212,0.15);
border-radius:var(--radius-sm);font-size:13px;color:var(--text-secondary);"> border-radius:var(--radius-sm);font-size:13px;color:var(--text-secondary);">
💡 <strong>Tip:</strong> Uncheck large data disks (e.g. video storage) to skip them. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 4px;"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
<strong>Tip:</strong> Uncheck large data disks (e.g. video storage) to skip them.
The VM config (.vmx) is always included. For ipcam VMs, keep only the small OS disk checked. The VM config (.vmx) is always included. For ipcam VMs, keep only the small OS disk checked.
</div> </div>
</div> </div>
@ -174,11 +179,17 @@
<!-- Destination --> <!-- Destination -->
<div class="section-card"> <div class="section-card">
<div class="section-card-header">📁 Destination</div> <div class="section-card-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
Destination
</div>
<div class="section-card-body"> <div class="section-card-body">
<!-- NFS quick-select --> <!-- NFS quick-select -->
<div id="nfsTargets" style="margin-bottom:14px; display:none;"> <div id="nfsTargets" style="margin-bottom:14px; display:none;">
<div class="form-label" style="margin-bottom:6px;">📡 NFS Mounts (click to use as destination)</div> <div class="form-label" style="margin-bottom:6px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
NFS Mounts (click to use as destination)
</div>
<div id="nfsMountList" style="display:flex; gap:8px; flex-wrap:wrap;"></div> <div id="nfsMountList" style="display:flex; gap:8px; flex-wrap:wrap;"></div>
</div> </div>
@ -223,14 +234,19 @@
<!-- Schedule --> <!-- Schedule -->
<div class="section-card"> <div class="section-card">
<div class="section-card-header">🕐 Schedule</div> <div class="section-card-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Schedule
</div>
<div class="section-card-body"> <div class="section-card-body">
<div class="schedule-options"> <div class="schedule-options">
<label class="schedule-opt {% if not show_schedule %}selected{% endif %}" id="opt-now" onclick="selectSchedule('now')"> <label class="schedule-opt {% if not show_schedule %}selected{% endif %}" id="opt-now" onclick="selectSchedule('now')">
<input type="radio" name="schedule_type" value="now" {% if not show_schedule %}checked{% endif %} /> <input type="radio" name="schedule_type" value="now" {% if not show_schedule %}checked{% endif %} />
<div> <div>
<div class="schedule-opt-icon"></div> <div class="schedule-opt-icon" style="height: 22px; display: flex; align-items: center;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
</div>
<div class="schedule-opt-title">Run Now</div> <div class="schedule-opt-title">Run Now</div>
<div class="schedule-opt-desc">Start the backup immediately</div> <div class="schedule-opt-desc">Start the backup immediately</div>
</div> </div>
@ -239,7 +255,9 @@
<label class="schedule-opt {% if show_schedule %}selected{% endif %}" id="opt-daily" onclick="selectSchedule('daily')"> <label class="schedule-opt {% if show_schedule %}selected{% endif %}" id="opt-daily" onclick="selectSchedule('daily')">
<input type="radio" name="schedule_type" value="daily" {% if show_schedule %}checked{% endif %}/> <input type="radio" name="schedule_type" value="daily" {% if show_schedule %}checked{% endif %}/>
<div> <div>
<div class="schedule-opt-icon">📅</div> <div class="schedule-opt-icon" style="height: 22px; display: flex; align-items: center;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
</div>
<div class="schedule-opt-title">Daily</div> <div class="schedule-opt-title">Daily</div>
<div class="schedule-opt-desc">Repeat every day at a set time</div> <div class="schedule-opt-desc">Repeat every day at a set time</div>
</div> </div>
@ -248,7 +266,9 @@
<label class="schedule-opt" id="opt-weekly" onclick="selectSchedule('weekly')"> <label class="schedule-opt" id="opt-weekly" onclick="selectSchedule('weekly')">
<input type="radio" name="schedule_type" value="weekly" /> <input type="radio" name="schedule_type" value="weekly" />
<div> <div>
<div class="schedule-opt-icon">🗓</div> <div class="schedule-opt-icon" style="height: 22px; display: flex; align-items: center;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg>
</div>
<div class="schedule-opt-title">Weekly</div> <div class="schedule-opt-title">Weekly</div>
<div class="schedule-opt-desc">Repeat every week on a specific day</div> <div class="schedule-opt-desc">Repeat every week on a specific day</div>
</div> </div>
@ -257,7 +277,9 @@
<label class="schedule-opt" id="opt-interval" onclick="selectSchedule('interval')"> <label class="schedule-opt" id="opt-interval" onclick="selectSchedule('interval')">
<input type="radio" name="schedule_type" value="interval" /> <input type="radio" name="schedule_type" value="interval" />
<div> <div>
<div class="schedule-opt-icon">🔁</div> <div class="schedule-opt-icon" style="height: 22px; display: flex; align-items: center;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--accent);"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
</div>
<div class="schedule-opt-title">Interval</div> <div class="schedule-opt-title">Interval</div>
<div class="schedule-opt-desc">Repeat every N hours</div> <div class="schedule-opt-desc">Repeat every N hours</div>
</div> </div>
@ -319,7 +341,10 @@
<div class="action-bar"> <div class="action-bar">
<button type="submit" id="submitBtn" class="btn btn-primary"> <button type="submit" id="submitBtn" class="btn btn-primary">
<span id="submitText">🚀 Create Job</span> <span id="submitText">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle;"><polyline points="20 6 9 17 4 12"/></svg>
Create Job
</span>
<span id="submitSpinner" class="spinner" style="display:none;"></span> <span id="submitSpinner" class="spinner" style="display:none;"></span>
</button> </button>
<a href="/vms" class="btn btn-ghost">Cancel</a> <a href="/vms" class="btn btn-ghost">Cancel</a>
@ -421,10 +446,10 @@
</div>`; </div>`;
}); });
html += `<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;"> html += `<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;">
<button type="button" class="btn btn-secondary btn-sm" onclick="selectAllDisks(true)">All</button> <button type="button" class="btn btn-secondary btn-sm" onclick="selectAllDisks(true)">All</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="selectAllDisks(false)">None</button> <button type="button" class="btn btn-secondary btn-sm" onclick="selectAllDisks(false)">None</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="selectOsOnly()" <button type="button" class="btn btn-secondary btn-sm" onclick="selectOsOnly()"
title="Select only the smallest disk (usually the OS disk)">🖥 OS Only</button> title="Select only the smallest disk (usually the OS disk)">OS Only</button>
</div>`; </div>`;
document.getElementById('diskList').innerHTML = html; document.getElementById('diskList').innerHTML = html;
@ -435,7 +460,7 @@
.catch(() => { .catch(() => {
document.getElementById('diskLoader').style.display = 'none'; document.getElementById('diskLoader').style.display = 'none';
document.getElementById('diskList').innerHTML = document.getElementById('diskList').innerHTML =
'<div style="color:var(--text-muted);font-size:13px;">Failed to load disk list</div>'; '<div style="color:var(--text-muted);font-size:13px;">Failed to load disk list</div>';
}); });
} }
@ -463,7 +488,7 @@
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.type = 'button'; btn.type = 'button';
btn.className = 'btn btn-secondary btn-sm'; btn.className = 'btn btn-secondary btn-sm';
btn.innerHTML = `📡 ${m.mountpoint} <span style="color:var(--text-muted);font-size:11px;margin-left:4px;">${m.free_gb}GB free</span>`; btn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle;"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg> ${m.mountpoint} <span style="color:var(--text-muted);font-size:11px;margin-left:4px;">${m.free_gb}GB free</span>`;
btn.onclick = () => { btn.onclick = () => {
document.getElementById('dest').value = m.mountpoint; document.getElementById('dest').value = m.mountpoint;
list.querySelectorAll('button').forEach(b => b.style.borderColor = ''); list.querySelectorAll('button').forEach(b => b.style.borderColor = '');

View File

@ -128,13 +128,13 @@
{{ job.label or 'Backup Job' }} {{ job.label or 'Backup Job' }}
<span id="statusBadge"> <span id="statusBadge">
{% if job.status == 'running' %} {% if job.status == 'running' %}
<span class="badge badge-purple running-badge" style="margin-left:10px; vertical-align:middle;">Running</span> <span class="badge badge-purple running-badge" style="margin-left:10px; vertical-align:middle;">Running</span>
{% elif job.status == 'finished' %} {% elif job.status == 'finished' %}
<span class="badge badge-green" style="margin-left:10px; vertical-align:middle;">Finished</span> <span class="badge badge-green" style="margin-left:10px; vertical-align:middle;">Finished</span>
{% elif job.status == 'queued' %} {% elif job.status == 'queued' %}
<span class="badge badge-yellow" style="margin-left:10px; vertical-align:middle;">Queued</span> <span class="badge badge-yellow" style="margin-left:10px; vertical-align:middle;">Queued</span>
{% elif job.status.startswith('failed') %} {% elif job.status.startswith('failed') %}
<span class="badge badge-red" style="margin-left:10px; vertical-align:middle;">Failed</span> <span class="badge badge-red" style="margin-left:10px; vertical-align:middle;">Failed</span>
{% else %} {% else %}
<span class="badge badge-gray" style="margin-left:10px; vertical-align:middle;">{{ job.status }}</span> <span class="badge badge-gray" style="margin-left:10px; vertical-align:middle;">{{ job.status }}</span>
{% endif %} {% endif %}
@ -143,7 +143,10 @@
<div class="topbar-subtitle">Job ID: <span class="mono">{{ job.id }}</span></div> <div class="topbar-subtitle">Job ID: <span class="mono">{{ job.id }}</span></div>
</div> </div>
<div class="topbar-actions"> <div class="topbar-actions">
<a href="/jobs" class="btn btn-ghost btn-sm">← All Jobs</a> <a href="/jobs" class="btn btn-ghost btn-sm">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
All Jobs
</a>
</div> </div>
</div> </div>
@ -162,7 +165,8 @@
<div class="detail-item-label">Schedule</div> <div class="detail-item-label">Schedule</div>
<div class="detail-item-val"> <div class="detail-item-val">
{% if job.schedule_type and job.schedule_type != 'now' %} {% if job.schedule_type and job.schedule_type != 'now' %}
🔁 {{ job.schedule_type|capitalize }} <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
{{ job.schedule_type|capitalize }}
{% if job.schedule_time %}at {{ job.schedule_time }}{% endif %} {% if job.schedule_time %}at {{ job.schedule_time }}{% endif %}
{% else %} {% else %}
One-time (Run Now) One-time (Run Now)
@ -179,9 +183,18 @@
</div> </div>
<div class="detail-item"> <div class="detail-item">
<div class="detail-item-label">Options</div> <div class="detail-item-label">Options</div>
<div class="detail-item-val" style="font-size:13px;"> <div class="detail-item-val" style="font-size:13px; display:flex; align-items:center; gap:6px;">
{% if job.compress %}🗜 Compressed{% else %}Raw{% endif %} {% if job.compress %}
{% if job.sftp_host %} · 📤 SFTP: {{ job.sftp_host }}{% endif %} <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle;"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/><path d="M12 11v4"/><path d="M10 13h4"/></svg>
Compressed
{% else %}
Raw
{% endif %}
{% if job.sftp_host %}
·
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle;"><path d="M21.2 15c.18-.52.3-1.07.3-1.64C21.5 10.4 18.9 8 15.6 8c-1.06 0-2.07.3-2.93.85C11.8 6.5 9 5 5.8 5 3.1 5 1 7.1 1 9.8c0 .28.02.55.07.82C.46 11.2 0 12.05 0 13c0 2.2 1.8 4 4 4h16c1.66 0 3-1.34 3-3 0-1.3-.84-2.4-2-2.82z"/><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/></svg>
SFTP: {{ job.sftp_host }}
{% endif %}
</div> </div>
</div> </div>
<div class="detail-item"> <div class="detail-item">
@ -200,7 +213,8 @@
{% if job.schedule_id %} {% if job.schedule_id %}
<div class="alert alert-info" style="margin-bottom:20px;"> <div class="alert alert-info" style="margin-bottom:20px;">
🔁 This job has an active recurring schedule. Future backups will run automatically. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 8px;"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
This job has an active recurring schedule. Future backups will run automatically.
<form method="post" action="/job/{{ job.id }}/cancel-schedule" <form method="post" action="/job/{{ job.id }}/cancel-schedule"
style="display:inline; margin-left:12px;" style="display:inline; margin-left:12px;"
onsubmit="return confirm('Cancel recurring schedule?')"> onsubmit="return confirm('Cancel recurring schedule?')">
@ -212,7 +226,10 @@
<!-- ── Progress card ── --> <!-- ── Progress card ── -->
<div class="progress-card" id="progressCard"> <div class="progress-card" id="progressCard">
<div class="progress-header"> <div class="progress-header">
<div class="progress-title">⚡ Backup Progress</div> <div class="progress-title" style="display:flex; align-items:center; gap:6px;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--accent-2);"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
Backup Progress
</div>
<div class="progress-pct" id="progressPct">0%</div> <div class="progress-pct" id="progressPct">0%</div>
</div> </div>
<div class="progress-bar-wrap"> <div class="progress-bar-wrap">
@ -220,24 +237,30 @@
</div> </div>
<div class="progress-detail" id="progressDetail">Waiting to start…</div> <div class="progress-detail" id="progressDetail">Waiting to start…</div>
<div class="phase-steps"> <div class="phase-steps">
<span class="phase-step" id="phase-connecting">🔌 Connect</span> <span class="phase-step" id="phase-connecting">Connect</span>
<span class="phase-step" id="phase-snapshot">📸 Snapshot</span> <span class="phase-step" id="phase-snapshot">Snapshot</span>
<span class="phase-step" id="phase-downloading">Download</span> <span class="phase-step" id="phase-downloading">Download</span>
<span class="phase-step" id="phase-compressing">🗜 Compress</span> <span class="phase-step" id="phase-compressing">Compress</span>
<span class="phase-step" id="phase-uploading">📤 Upload</span> <span class="phase-step" id="phase-uploading">Upload</span>
<span class="phase-step" id="phase-cleanup">🧹 Cleanup</span> <span class="phase-step" id="phase-cleanup">Cleanup</span>
<span class="phase-step" id="phase-done">Done</span> <span class="phase-step" id="phase-done">Done</span>
</div> </div>
</div> </div>
<!-- ── Log viewer ── --> <!-- ── Log viewer ── -->
<div class="log-wrap"> <div class="log-wrap">
<div class="log-toolbar"> <div class="log-toolbar">
<div class="log-title">📄 Backup Log</div> <div class="log-title" style="display:flex; align-items:center; gap:6px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
Backup Log
</div>
<div class="flex-center gap-2"> <div class="flex-center gap-2">
<span id="spinnerWrap" class="spinner" style="display:none;"></span> <span id="spinnerWrap" class="spinner" style="display:none;"></span>
<span id="autoRefreshLabel" class="text-small text-muted" style="display:none;">Live</span> <span id="autoRefreshLabel" class="text-small text-muted" style="display:none;">Live</span>
<button class="btn btn-ghost btn-sm" onclick="scrollLogBottom()">⬇ Bottom</button> <button class="btn btn-ghost btn-sm" onclick="scrollLogBottom()">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px;"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg>
Bottom
</button>
</div> </div>
</div> </div>
<pre id="logContent">(Loading…)</pre> <pre id="logContent">(Loading…)</pre>
@ -285,10 +308,10 @@
function setStatusBadge(status) { function setStatusBadge(status) {
let html = ''; let html = '';
if (status === 'running') html = '<span class="badge badge-purple running-badge" style="margin-left:10px;vertical-align:middle;">Running</span>'; if (status === 'running') html = '<span class="badge badge-purple running-badge" style="margin-left:10px;vertical-align:middle;">Running</span>';
else if (status === 'finished') html = '<span class="badge badge-green" style="margin-left:10px;vertical-align:middle;">Finished</span>'; else if (status === 'finished') html = '<span class="badge badge-green" style="margin-left:10px;vertical-align:middle;">Finished</span>';
else if (status === 'queued') html = '<span class="badge badge-yellow" style="margin-left:10px;vertical-align:middle;">Queued</span>'; else if (status === 'queued') html = '<span class="badge badge-yellow" style="margin-left:10px;vertical-align:middle;">Queued</span>';
else if (status.startsWith('failed')) html = '<span class="badge badge-red" style="margin-left:10px;vertical-align:middle;">Failed</span>'; else if (status.startsWith('failed')) html = '<span class="badge badge-red" style="margin-left:10px;vertical-align:middle;">Failed</span>';
document.getElementById('statusBadge').innerHTML = html; document.getElementById('statusBadge').innerHTML = html;
document.getElementById('statusText').textContent = status; document.getElementById('statusText').textContent = status;
} }
@ -328,7 +351,7 @@
clearInterval(pollTimer); clearInterval(pollTimer);
// Show full-width done/fail indicator // Show full-width done/fail indicator
if (status === 'finished') { if (status === 'finished') {
setProgress({ pct: 100, phase: 'done', detail: 'Backup completed successfully' }); setProgress({ pct: 100, phase: 'done', detail: 'Backup completed successfully' });
} }
} }
} catch(e) {} } catch(e) {}
@ -344,7 +367,7 @@
const pollTimer = setInterval(poll, 2000); const pollTimer = setInterval(poll, 2000);
poll(); poll();
{% elif job.status == 'finished' %} {% elif job.status == 'finished' %}
setProgress({ pct: 100, phase: 'done', detail: 'Backup completed successfully' }); setProgress({ pct: 100, phase: 'done', detail: 'Backup completed successfully' });
{% elif job.status.startsWith('failed') %} {% elif job.status.startsWith('failed') %}
setProgress({ pct: 0, phase: 'failed', detail: '{{ job.status }}' }); setProgress({ pct: 0, phase: 'failed', detail: '{{ job.status }}' });
{% else %} {% else %}

View File

@ -70,7 +70,10 @@
<div class="topbar-subtitle">All scheduled and completed backup jobs</div> <div class="topbar-subtitle">All scheduled and completed backup jobs</div>
</div> </div>
<div class="topbar-actions"> <div class="topbar-actions">
<a href="/jobs/create" class="btn btn-primary btn-sm"> Create Job</a> <a href="/jobs/create" class="btn btn-primary btn-sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Create Job
</a>
</div> </div>
</div> </div>
@ -130,20 +133,23 @@
</td> </td>
<td> <td>
{% if job.status == 'running' %} {% if job.status == 'running' %}
<span class="badge badge-purple running-pulse">Running</span> <span class="badge badge-purple running-pulse">Running</span>
{% elif job.status == 'finished' %} {% elif job.status == 'finished' %}
<span class="badge badge-green">Finished</span> <span class="badge badge-green">Finished</span>
{% elif job.status == 'queued' %} {% elif job.status == 'queued' %}
<span class="badge badge-yellow">Queued</span> <span class="badge badge-yellow">Queued</span>
{% elif job.status.startswith('failed') %} {% elif job.status.startswith('failed') %}
<span class="badge badge-red" title="{{ job.status }}">Failed</span> <span class="badge badge-red" title="{{ job.status }}">Failed</span>
{% else %} {% else %}
<span class="badge badge-gray">{{ job.status }}</span> <span class="badge badge-gray">{{ job.status }}</span>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if job.schedule_type and job.schedule_type != 'now' %} {% if job.schedule_type and job.schedule_type != 'now' %}
<span class="schedule-tag">🔁 {{ job.schedule_type|capitalize }}</span> <span class="schedule-tag">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px;"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
{{ job.schedule_type|capitalize }}
</span>
{% else %} {% else %}
<span class="text-muted text-small">One-time</span> <span class="text-muted text-small">One-time</span>
{% endif %} {% endif %}
@ -170,9 +176,14 @@
{% else %} {% else %}
<div class="empty-state"> <div class="empty-state">
<div class="empty-icon">📋</div> <div class="empty-icon" style="display: flex; justify-content: center; margin-bottom: 16px;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="color: var(--text-muted);"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/></svg>
</div>
<p>No backup jobs yet.</p> <p>No backup jobs yet.</p>
<a href="/jobs/create" class="btn btn-primary" style="margin-top:16px;"> Create your first job</a> <a href="/jobs/create" class="btn btn-primary" style="margin-top:16px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Create your first job
</a>
</div> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -56,7 +56,14 @@
{% block content %} {% block content %}
<div class="login-wrap"> <div class="login-wrap">
<div class="login-header"> <div class="login-header">
<div class="login-icon">🛡</div> <div class="login-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="color: #ffffff; display: block;">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
<path d="M8 11a4 4 0 0 1 6-3.46M16 13a4 4 0 0 1-6 3.46"/>
<path d="M12 6h2v2"/>
<path d="M12 18H10v-2"/>
</svg>
</div>
<h1>vSphere Backup Manager</h1> <h1>vSphere Backup Manager</h1>
<p>Connect to your vCenter or ESXi host to get started</p> <p>Connect to your vCenter or ESXi host to get started</p>
</div> </div>

View File

@ -97,23 +97,30 @@
<div class="topbar-subtitle">Manage network storage mounts used as backup targets</div> <div class="topbar-subtitle">Manage network storage mounts used as backup targets</div>
</div> </div>
<div class="topbar-actions"> <div class="topbar-actions">
<a href="/nfs" class="btn btn-ghost btn-sm">🔄 Refresh</a> <a href="/nfs" class="btn btn-ghost btn-sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
Refresh
</a>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
{% if not is_linux %} {% if not is_linux %}
<div class="linux-warn"> <div class="linux-warn" style="display:flex; align-items:center; gap:10px;">
⚠ NFS management requires Linux. This server is running on <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
<div>
NFS management requires Linux. This server is running on
<strong>{{ request.environ.get('SERVER_SOFTWARE', 'non-Linux OS') }}</strong>. <strong>{{ request.environ.get('SERVER_SOFTWARE', 'non-Linux OS') }}</strong>.
Mount/unmount operations will work on the deployed Linux server. Mount/unmount operations will work on the deployed Linux server.
</div> </div>
</div>
{% endif %} {% endif %}
<!-- Currently mounted shares --> <!-- Currently mounted shares -->
<h3 style="font-size:15px; font-weight:600; margin-bottom:14px;"> <h3 style="font-size:15px; font-weight:600; margin-bottom:14px; display:flex; align-items:center; gap:8px;">
📡 Mounted Shares <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
Mounted Shares
{% if mounts %} {% if mounts %}
<span class="badge badge-green" style="margin-left:8px; vertical-align:middle;">{{ mounts|length }} active</span> <span class="badge badge-green" style="margin-left:8px; vertical-align:middle;">{{ mounts|length }} active</span>
{% endif %} {% endif %}
@ -153,11 +160,17 @@
<div class="nfs-actions"> <div class="nfs-actions">
<a href="/jobs/create?dest={{ m.mountpoint|urlencode }}" <a href="/jobs/create?dest={{ m.mountpoint|urlencode }}"
class="btn btn-primary btn-sm">📋 Use as Target</a> class="btn btn-primary btn-sm">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px;"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><rect x="8" y="2" width="8" height="4" rx="1" ry="1"/></svg>
Use as Target
</a>
<form method="post" action="/nfs/umount" <form method="post" action="/nfs/umount"
onsubmit="return confirm('Unmount {{ m.mountpoint }}?')"> onsubmit="return confirm('Unmount {{ m.mountpoint }}?')">
<input type="hidden" name="mountpoint" value="{{ m.mountpoint }}" /> <input type="hidden" name="mountpoint" value="{{ m.mountpoint }}" />
<button class="btn btn-danger btn-sm" type="submit">⏏ Unmount</button> <button class="btn btn-danger btn-sm" type="submit">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 4px;"><polygon points="12 2 2 14 22 14"/><path d="M2 20h20"/></svg>
Unmount
</button>
</form> </form>
</div> </div>
</div> </div>
@ -165,13 +178,18 @@
</div> </div>
{% else %} {% else %}
<div class="empty-state" style="margin-bottom:28px;"> <div class="empty-state" style="margin-bottom:28px;">
<div class="empty-icon">📡</div> <div class="empty-icon" style="display: flex; justify-content: center; margin-bottom: 16px;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="color: var(--text-muted);"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
</div>
<p>No NFS shares currently mounted.</p> <p>No NFS shares currently mounted.</p>
</div> </div>
{% endif %} {% endif %}
<!-- Mount new share form --> <!-- Mount new share form -->
<h3 style="font-size:15px; font-weight:600; margin-bottom:14px;"> Mount New Share</h3> <h3 style="font-size:15px; font-weight:600; margin-bottom:14px; display:flex; align-items:center; gap:8px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: middle; margin-right: 6px;"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Mount New Share
</h3>
<div class="mount-form-card"> <div class="mount-form-card">
<form method="post" action="/nfs/mount" id="mountForm"> <form method="post" action="/nfs/mount" id="mountForm">
<div class="form-row-3" style="margin-bottom:14px;"> <div class="form-row-3" style="margin-bottom:14px;">
@ -209,7 +227,8 @@
</div> </div>
<button type="submit" class="btn btn-primary" {% if not is_linux %}disabled title="Linux only"{% endif %}> <button type="submit" class="btn btn-primary" {% if not is_linux %}disabled title="Linux only"{% endif %}>
📡 Mount Share <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px; vertical-align: middle;"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"/><rect x="2" y="14" width="20" height="8" rx="2" ry="2"/><line x1="6" y1="6" x2="6.01" y2="6"/><line x1="6" y1="18" x2="6.01" y2="18"/></svg>
Mount Share
</button> </button>
{% if not is_linux %} {% if not is_linux %}
<span class="text-muted text-small" style="margin-left:10px;">Disabled on non-Linux</span> <span class="text-muted text-small" style="margin-left:10px;">Disabled on non-Linux</span>

View File

@ -140,14 +140,23 @@
</div> </div>
</div> </div>
<div class="topbar-actions"> <div class="topbar-actions">
<a href="/vms?refresh=1" class="btn btn-ghost btn-sm">🔄 Refresh</a> <a href="/vms?refresh=1" class="btn btn-ghost btn-sm">
<a href="/jobs/create" class="btn btn-primary btn-sm"> Create Job</a> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><path d="M23 4v6h-6"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
Refresh
</a>
<a href="/jobs/create" class="btn btn-primary btn-sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Create Job
</a>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
{% if error %} {% if error %}
<div class="alert alert-danger">⚠ {{ error }}</div> <div class="alert alert-danger">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 8px;"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
{{ error }}
</div>
{% endif %} {% endif %}
<!-- Summary chips --> <!-- Summary chips -->
@ -173,13 +182,19 @@
<!-- Filter / Search row --> <!-- Filter / Search row -->
<div class="filter-row" style="margin-bottom:16px;"> <div class="filter-row" style="margin-bottom:16px;">
<div class="search-bar"> <div class="search-bar">
<span>🔍</span> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="color: var(--text-muted);"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="text" id="vmSearch" placeholder="Search VMs…" oninput="filterVMs()" /> <input type="text" id="vmSearch" placeholder="Search VMs…" oninput="filterVMs()" />
</div> </div>
<button class="filter-chip active" id="filter-all" onclick="setFilter('all')">All</button> <button class="filter-chip active" id="filter-all" onclick="setFilter('all')">All</button>
<button class="filter-chip" id="filter-poweredOn" onclick="setFilter('poweredOn')">🟢 On</button> <button class="filter-chip" id="filter-poweredOn" onclick="setFilter('poweredOn')">
<button class="filter-chip" id="filter-poweredOff" onclick="setFilter('poweredOff')">🔴 Off</button> <span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:var(--success); margin-right:6px; vertical-align:middle;"></span>On
<button class="filter-chip" id="filter-suspended" onclick="setFilter('suspended')">🟡 Suspended</button> </button>
<button class="filter-chip" id="filter-poweredOff" onclick="setFilter('poweredOff')">
<span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:var(--danger); margin-right:6px; vertical-align:middle;"></span>Off
</button>
<button class="filter-chip" id="filter-suspended" onclick="setFilter('suspended')">
<span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:var(--warning); margin-right:6px; vertical-align:middle;"></span>Suspended
</button>
</div> </div>
<!-- VM Cards grid --> <!-- VM Cards grid -->
@ -225,21 +240,30 @@
</div> </div>
{% if vm.datastores %} {% if vm.datastores %}
<div class="text-small text-muted mb-2"> <div class="text-small text-muted mb-2" style="display:flex; align-items:center; gap:6px;">
📦 {{ vm.datastores|join(', ') }} <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color: var(--text-muted);"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/><path d="M3 12c0 1.66 4 3 9 3s9-1.34 9-3"/></svg>
{{ vm.datastores|join(', ') }}
</div> </div>
{% endif %} {% endif %}
<div class="vm-footer"> <div class="vm-footer">
<a href="/jobs/create?vm={{ vm.name|urlencode }}" <a href="/jobs/create?vm={{ vm.name|urlencode }}"
class="btn btn-primary btn-sm">⚡ Backup Now</a> class="btn btn-primary btn-sm">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
Backup Now
</a>
<a href="/jobs/create?vm={{ vm.name|urlencode }}&schedule=1" <a href="/jobs/create?vm={{ vm.name|urlencode }}&schedule=1"
class="btn btn-secondary btn-sm">🕐 Schedule</a> class="btn btn-secondary btn-sm">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Schedule
</a>
</div> </div>
</div> </div>
{% else %} {% else %}
<div class="empty-state" style="grid-column:1/-1;"> <div class="empty-state" style="grid-column:1/-1;">
<div class="empty-icon">🖥</div> <div class="empty-icon" style="display: flex; justify-content: center; margin-bottom: 16px;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="color: var(--text-muted);"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
</div>
<p>No virtual machines found.</p> <p>No virtual machines found.</p>
</div> </div>
{% endfor %} {% endfor %}