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);
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 {
padding: 20px 24px;
@ -344,7 +344,14 @@
<aside class="sidebar">
<div class="sidebar-logo">
<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 class="logo-text">vSphere Backup</div>
<div class="logo-sub">Manager</div>
@ -355,16 +362,28 @@
<nav class="sidebar-nav">
<div class="nav-section-label">Navigation</div>
<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 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 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 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>
</nav>
@ -374,13 +393,14 @@
<div class="server-user text-small">{{ session.get('user', '') }}</div>
</div>
<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>
</div>
</aside>
{% 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) %}
{% if messages %}
<div style="padding:16px 32px 0">

View File

@ -131,7 +131,10 @@
<!-- VM Selection -->
<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="form-group" style="margin:0;">
<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="" />
<div class="section-card" id="diskCard" style="display:none;">
<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>
</div>
<div class="section-card-body">
@ -166,7 +170,8 @@
<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);
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.
</div>
</div>
@ -174,11 +179,17 @@
<!-- Destination -->
<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">
<!-- NFS quick-select -->
<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>
@ -223,14 +234,19 @@
<!-- Schedule -->
<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="schedule-options">
<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 %} />
<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-desc">Start the backup immediately</div>
</div>
@ -239,7 +255,9 @@
<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 %}/>
<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-desc">Repeat every day at a set time</div>
</div>
@ -248,7 +266,9 @@
<label class="schedule-opt" id="opt-weekly" onclick="selectSchedule('weekly')">
<input type="radio" name="schedule_type" value="weekly" />
<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-desc">Repeat every week on a specific day</div>
</div>
@ -257,7 +277,9 @@
<label class="schedule-opt" id="opt-interval" onclick="selectSchedule('interval')">
<input type="radio" name="schedule_type" value="interval" />
<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-desc">Repeat every N hours</div>
</div>
@ -319,7 +341,10 @@
<div class="action-bar">
<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>
</button>
<a href="/vms" class="btn btn-ghost">Cancel</a>
@ -421,10 +446,10 @@
</div>`;
});
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(false)">None</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="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>`;
document.getElementById('diskList').innerHTML = html;
@ -435,7 +460,7 @@
.catch(() => {
document.getElementById('diskLoader').style.display = 'none';
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');
btn.type = 'button';
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 = () => {
document.getElementById('dest').value = m.mountpoint;
list.querySelectorAll('button').forEach(b => b.style.borderColor = '');

View File

@ -128,13 +128,13 @@
{{ job.label or 'Backup Job' }}
<span id="statusBadge">
{% 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' %}
<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' %}
<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') %}
<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 %}
<span class="badge badge-gray" style="margin-left:10px; vertical-align:middle;">{{ job.status }}</span>
{% endif %}
@ -143,7 +143,10 @@
<div class="topbar-subtitle">Job ID: <span class="mono">{{ job.id }}</span></div>
</div>
<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>
@ -162,7 +165,8 @@
<div class="detail-item-label">Schedule</div>
<div class="detail-item-val">
{% 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 %}
{% else %}
One-time (Run Now)
@ -179,9 +183,18 @@
</div>
<div class="detail-item">
<div class="detail-item-label">Options</div>
<div class="detail-item-val" style="font-size:13px;">
{% if job.compress %}🗜 Compressed{% else %}Raw{% endif %}
{% if job.sftp_host %} · 📤 SFTP: {{ job.sftp_host }}{% endif %}
<div class="detail-item-val" style="font-size:13px; display:flex; align-items:center; gap:6px;">
{% if job.compress %}
<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 class="detail-item">
@ -200,7 +213,8 @@
{% if job.schedule_id %}
<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"
style="display:inline; margin-left:12px;"
onsubmit="return confirm('Cancel recurring schedule?')">
@ -212,7 +226,10 @@
<!-- ── Progress card ── -->
<div class="progress-card" id="progressCard">
<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>
<div class="progress-bar-wrap">
@ -220,24 +237,30 @@
</div>
<div class="progress-detail" id="progressDetail">Waiting to start…</div>
<div class="phase-steps">
<span class="phase-step" id="phase-connecting">🔌 Connect</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-compressing">🗜 Compress</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-done">Done</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-downloading">Download</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-cleanup">Cleanup</span>
<span class="phase-step" id="phase-done">Done</span>
</div>
</div>
<!-- ── Log viewer ── -->
<div class="log-wrap">
<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">
<span id="spinnerWrap" class="spinner" style="display:none;"></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>
<pre id="logContent">(Loading…)</pre>
@ -285,10 +308,10 @@
function setStatusBadge(status) {
let html = '';
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 === '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>';
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 === '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>';
document.getElementById('statusBadge').innerHTML = html;
document.getElementById('statusText').textContent = status;
}
@ -328,7 +351,7 @@
clearInterval(pollTimer);
// Show full-width done/fail indicator
if (status === 'finished') {
setProgress({ pct: 100, phase: 'done', detail: 'Backup completed successfully' });
setProgress({ pct: 100, phase: 'done', detail: 'Backup completed successfully' });
}
}
} catch(e) {}
@ -344,7 +367,7 @@
const pollTimer = setInterval(poll, 2000);
poll();
{% 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') %}
setProgress({ pct: 0, phase: 'failed', detail: '{{ job.status }}' });
{% else %}

View File

@ -70,7 +70,10 @@
<div class="topbar-subtitle">All scheduled and completed backup jobs</div>
</div>
<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>
@ -130,20 +133,23 @@
</td>
<td>
{% 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' %}
<span class="badge badge-green">Finished</span>
<span class="badge badge-green">Finished</span>
{% elif job.status == 'queued' %}
<span class="badge badge-yellow">Queued</span>
<span class="badge badge-yellow">Queued</span>
{% 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 %}
<span class="badge badge-gray">{{ job.status }}</span>
{% endif %}
</td>
<td>
{% 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 %}
<span class="text-muted text-small">One-time</span>
{% endif %}
@ -170,9 +176,14 @@
{% else %}
<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>
<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>
{% endif %}
</div>

View File

@ -56,7 +56,14 @@
{% block content %}
<div class="login-wrap">
<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>
<p>Connect to your vCenter or ESXi host to get started</p>
</div>

View File

@ -97,23 +97,30 @@
<div class="topbar-subtitle">Manage network storage mounts used as backup targets</div>
</div>
<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 class="content">
{% if not is_linux %}
<div class="linux-warn">
⚠ NFS management requires Linux. This server is running on
<strong>{{ request.environ.get('SERVER_SOFTWARE', 'non-Linux OS') }}</strong>.
Mount/unmount operations will work on the deployed Linux server.
<div class="linux-warn" style="display:flex; align-items:center; gap:10px;">
<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>.
Mount/unmount operations will work on the deployed Linux server.
</div>
</div>
{% endif %}
<!-- Currently mounted shares -->
<h3 style="font-size:15px; font-weight:600; margin-bottom:14px;">
📡 Mounted Shares
<h3 style="font-size:15px; font-weight:600; margin-bottom:14px; display:flex; align-items:center; gap:8px;">
<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 %}
<span class="badge badge-green" style="margin-left:8px; vertical-align:middle;">{{ mounts|length }} active</span>
{% endif %}
@ -153,11 +160,17 @@
<div class="nfs-actions">
<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"
onsubmit="return confirm('Unmount {{ 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>
</div>
</div>
@ -165,13 +178,18 @@
</div>
{% else %}
<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>
</div>
{% endif %}
<!-- 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">
<form method="post" action="/nfs/mount" id="mountForm">
<div class="form-row-3" style="margin-bottom:14px;">
@ -209,7 +227,8 @@
</div>
<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>
{% if not is_linux %}
<span class="text-muted text-small" style="margin-left:10px;">Disabled on non-Linux</span>

View File

@ -140,14 +140,23 @@
</div>
</div>
<div class="topbar-actions">
<a href="/vms?refresh=1" class="btn btn-ghost btn-sm">🔄 Refresh</a>
<a href="/jobs/create" class="btn btn-primary btn-sm"> Create Job</a>
<a href="/vms?refresh=1" 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>
<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 class="content">
{% 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 %}
<!-- Summary chips -->
@ -173,13 +182,19 @@
<!-- Filter / Search row -->
<div class="filter-row" style="margin-bottom:16px;">
<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()" />
</div>
<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-poweredOff" onclick="setFilter('poweredOff')">🔴 Off</button>
<button class="filter-chip" id="filter-suspended" onclick="setFilter('suspended')">🟡 Suspended</button>
<button class="filter-chip" id="filter-poweredOn" onclick="setFilter('poweredOn')">
<span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:var(--success); margin-right:6px; vertical-align:middle;"></span>On
</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>
<!-- VM Cards grid -->
@ -225,21 +240,30 @@
</div>
{% if vm.datastores %}
<div class="text-small text-muted mb-2">
📦 {{ vm.datastores|join(', ') }}
<div class="text-small text-muted mb-2" style="display:flex; align-items:center; gap:6px;">
<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>
{% endif %}
<div class="vm-footer">
<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"
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>
{% else %}
<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>
</div>
{% endfor %}