763 lines
30 KiB
HTML
763 lines
30 KiB
HTML
{{define "protection-content"}}
|
|
<div class="space-y-6">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-white mb-2">Data Protection</h1>
|
|
<p class="text-slate-400">Manage snapshots, scheduling, and replication</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="border-b border-slate-800">
|
|
<nav class="flex gap-4">
|
|
<button onclick="switchTab('snapshots')" id="tab-snapshots" class="tab-button px-4 py-2 border-b-2 border-blue-600 text-blue-400 font-medium">
|
|
Snapshots
|
|
</button>
|
|
<button onclick="switchTab('policies')" id="tab-policies" class="tab-button px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300">
|
|
Snapshot Policies
|
|
</button>
|
|
<button onclick="switchTab('volume-snapshots')" id="tab-volume-snapshots" class="tab-button px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300">
|
|
Volume Snapshots
|
|
</button>
|
|
<button onclick="switchTab('replication')" id="tab-replication" class="tab-button px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300">
|
|
Replication
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Snapshots Tab -->
|
|
<div id="content-snapshots" class="tab-content">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
|
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-white">Snapshots</h2>
|
|
<div class="flex gap-2">
|
|
<button onclick="showCreateSnapshotModal()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create Snapshot
|
|
</button>
|
|
<button onclick="loadSnapshots()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div id="snapshots-list" class="p-4">
|
|
<p class="text-slate-400 text-sm">Loading...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Snapshot Policies Tab -->
|
|
<div id="content-policies" class="tab-content hidden">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
|
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-white">Snapshot Policies</h2>
|
|
<div class="flex gap-2">
|
|
<button onclick="showCreatePolicyModal()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create Policy
|
|
</button>
|
|
<button onclick="loadPolicies()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div id="policies-list" class="p-4">
|
|
<p class="text-slate-400 text-sm">Loading...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Volume Snapshots Tab -->
|
|
<div id="content-volume-snapshots" class="tab-content hidden">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
|
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-white">Volume Snapshots</h2>
|
|
<div class="flex gap-2">
|
|
<button onclick="showCreateVolumeSnapshotModal()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create Volume Snapshot
|
|
</button>
|
|
<button onclick="loadVolumeSnapshots()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div id="volume-snapshots-list" class="p-4">
|
|
<p class="text-slate-400 text-sm">Loading...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Replication Tab -->
|
|
<div id="content-replication" class="tab-content hidden">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
|
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-white">Replication</h2>
|
|
<div class="flex gap-2">
|
|
<button onclick="showCreateReplicationModal()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create Replication Task
|
|
</button>
|
|
<button onclick="loadReplications()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div id="replications-list" class="p-4">
|
|
<div class="text-center py-8">
|
|
<p class="text-slate-400 text-sm mb-2">Replication feature coming soon</p>
|
|
<p class="text-slate-500 text-xs">This feature will allow you to replicate snapshots to remote systems</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Snapshot Modal -->
|
|
<div id="create-snapshot-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-md w-full mx-4">
|
|
<h3 class="text-xl font-semibold text-white mb-4">Create Snapshot</h3>
|
|
<form id="create-snapshot-form" onsubmit="createSnapshot(event)" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Dataset</label>
|
|
<select name="dataset" id="snapshot-dataset-select" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
<option value="">Loading datasets...</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Snapshot Name</label>
|
|
<input type="text" name="name" placeholder="snapshot-2024-12-15" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="flex items-center gap-2 text-sm text-slate-300">
|
|
<input type="checkbox" name="recursive" class="rounded bg-slate-900 border-slate-700">
|
|
<span>Recursive (include child datasets)</span>
|
|
</label>
|
|
</div>
|
|
<div class="flex gap-2 justify-end">
|
|
<button type="button" onclick="closeModal('create-snapshot-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Snapshot Policy Modal -->
|
|
<div id="create-policy-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
|
|
<h3 class="text-xl font-semibold text-white mb-4">Create Snapshot Policy</h3>
|
|
<form id="create-policy-form" onsubmit="createPolicy(event)" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Dataset</label>
|
|
<select name="dataset" id="policy-dataset-select" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
<option value="">Loading datasets...</option>
|
|
</select>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Frequent (15min)</label>
|
|
<input type="number" name="frequent" min="0" value="0" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Hourly</label>
|
|
<input type="number" name="hourly" min="0" value="0" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Daily</label>
|
|
<input type="number" name="daily" min="0" value="0" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Weekly</label>
|
|
<input type="number" name="weekly" min="0" value="0" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Monthly</label>
|
|
<input type="number" name="monthly" min="0" value="0" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Yearly</label>
|
|
<input type="number" name="yearly" min="0" value="0" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
</div>
|
|
<div class="space-y-2">
|
|
<label class="flex items-center gap-2 text-sm text-slate-300">
|
|
<input type="checkbox" name="autosnap" checked class="rounded bg-slate-900 border-slate-700">
|
|
<span>Enable automatic snapshots</span>
|
|
</label>
|
|
<label class="flex items-center gap-2 text-sm text-slate-300">
|
|
<input type="checkbox" name="autoprune" checked class="rounded bg-slate-900 border-slate-700">
|
|
<span>Enable automatic pruning</span>
|
|
</label>
|
|
</div>
|
|
<div class="flex gap-2 justify-end">
|
|
<button type="button" onclick="closeModal('create-policy-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Volume Snapshot Modal -->
|
|
<div id="create-volume-snapshot-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
<div class="bg-slate-800 rounded-lg border border-slate-700 p-6 max-w-md w-full mx-4">
|
|
<h3 class="text-xl font-semibold text-white mb-4">Create Volume Snapshot</h3>
|
|
<form id="create-volume-snapshot-form" onsubmit="createVolumeSnapshot(event)" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Volume</label>
|
|
<select name="volume" id="volume-snapshot-select" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
<option value="">Loading volumes...</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Snapshot Name</label>
|
|
<input type="text" name="name" placeholder="volume-snapshot-2024-12-15" required class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
|
|
</div>
|
|
<div class="flex gap-2 justify-end">
|
|
<button type="button" onclick="closeModal('create-volume-snapshot-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
|
Cancel
|
|
</button>
|
|
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Create
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Check authentication on page load
|
|
(function() {
|
|
const token = localStorage.getItem('atlas_token');
|
|
if (!token) {
|
|
// No token, redirect to login
|
|
window.location.href = '/login?return=' + encodeURIComponent(window.location.pathname);
|
|
return;
|
|
}
|
|
})();
|
|
|
|
function getAuthHeaders() {
|
|
const token = localStorage.getItem('atlas_token');
|
|
return {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
|
};
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (!bytes || bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return 'N/A';
|
|
try {
|
|
return new Date(dateStr).toLocaleString();
|
|
} catch {
|
|
return dateStr;
|
|
}
|
|
}
|
|
|
|
function switchTab(tab) {
|
|
// Hide all tabs
|
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
|
document.querySelectorAll('.tab-button').forEach(el => {
|
|
el.classList.remove('border-blue-600', 'text-blue-400', 'font-medium');
|
|
el.classList.add('border-transparent', 'text-slate-400');
|
|
});
|
|
|
|
// Show selected tab
|
|
document.getElementById(`content-${tab}`).classList.remove('hidden');
|
|
document.getElementById(`tab-${tab}`).classList.remove('border-transparent', 'text-slate-400');
|
|
document.getElementById(`tab-${tab}`).classList.add('border-blue-600', 'text-blue-400', 'font-medium');
|
|
|
|
// Load data for the tab
|
|
if (tab === 'snapshots') loadSnapshots();
|
|
else if (tab === 'policies') loadPolicies();
|
|
else if (tab === 'volume-snapshots') loadVolumeSnapshots();
|
|
else if (tab === 'replication') loadReplications();
|
|
}
|
|
|
|
function closeModal(modalId) {
|
|
document.getElementById(modalId).classList.add('hidden');
|
|
}
|
|
|
|
// Snapshot Management
|
|
async function loadSnapshots() {
|
|
try {
|
|
const res = await fetch('/api/v1/snapshots', { headers: getAuthHeaders() });
|
|
const data = await res.json().catch(() => null);
|
|
const listEl = document.getElementById('snapshots-list');
|
|
|
|
if (!res.ok) {
|
|
const errorMsg = (data && data.error) ? data.error : `HTTP ${res.status}`;
|
|
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${errorMsg}</p>`;
|
|
return;
|
|
}
|
|
|
|
if (!data || !Array.isArray(data)) {
|
|
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
|
|
return;
|
|
}
|
|
|
|
if (data.length === 0) {
|
|
listEl.innerHTML = '<p class="text-slate-400 text-sm">No snapshots found</p>';
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = data.map(snap => `
|
|
<div class="border-b border-slate-700 last:border-0 py-4">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<h3 class="text-lg font-semibold text-white font-mono">${snap.name}</h3>
|
|
</div>
|
|
<div class="text-sm text-slate-400 space-y-1">
|
|
<p>Dataset: <span class="text-slate-300">${snap.dataset}</span></p>
|
|
<p>Size: <span class="text-slate-300">${formatBytes(snap.size || 0)}</span></p>
|
|
<p>Created: <span class="text-slate-300">${formatDate(snap.created_at)}</span></p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2 ml-4">
|
|
<button onclick="restoreSnapshot('${snap.name}', '${snap.dataset}')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Restore
|
|
</button>
|
|
<button onclick="deleteSnapshot('${snap.name}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (err) {
|
|
document.getElementById('snapshots-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function showCreateSnapshotModal() {
|
|
await loadDatasetsForSnapshot();
|
|
document.getElementById('create-snapshot-modal').classList.remove('hidden');
|
|
}
|
|
|
|
async function loadDatasetsForSnapshot() {
|
|
try {
|
|
const res = await fetch('/api/v1/datasets', { headers: getAuthHeaders() });
|
|
const selectEl = document.getElementById('snapshot-dataset-select');
|
|
|
|
if (!res.ok) {
|
|
selectEl.innerHTML = '<option value="">Error loading datasets</option>';
|
|
return;
|
|
}
|
|
|
|
const datasets = await res.json();
|
|
|
|
if (!Array.isArray(datasets) || datasets.length === 0) {
|
|
selectEl.innerHTML = '<option value="">No datasets found</option>';
|
|
return;
|
|
}
|
|
|
|
selectEl.innerHTML = '<option value="">Select a dataset...</option>';
|
|
datasets.forEach(ds => {
|
|
const option = document.createElement('option');
|
|
option.value = ds.name;
|
|
option.textContent = `${ds.name} (${ds.type})`;
|
|
selectEl.appendChild(option);
|
|
});
|
|
} catch (err) {
|
|
console.error('Error loading datasets:', err);
|
|
document.getElementById('snapshot-dataset-select').innerHTML = '<option value="">Error loading datasets</option>';
|
|
}
|
|
}
|
|
|
|
async function createSnapshot(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/snapshots', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
dataset: formData.get('dataset'),
|
|
name: formData.get('name'),
|
|
recursive: formData.get('recursive') === 'on'
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeModal('create-snapshot-modal');
|
|
e.target.reset();
|
|
loadSnapshots();
|
|
alert('Snapshot created successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to create snapshot'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Restore snapshot function - must be in global scope
|
|
window.restoreSnapshot = async function(snapshotName, datasetName) {
|
|
const warning = `WARNING: This will rollback dataset "${datasetName}" to snapshot "${snapshotName}".\n\n` +
|
|
`This action will:\n` +
|
|
`- Discard all changes made after this snapshot\n` +
|
|
`- Cannot be undone\n\n` +
|
|
`Are you sure you want to continue?`;
|
|
|
|
if (!confirm(warning)) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/snapshots/${encodeURIComponent(snapshotName)}/restore`, {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({ force: false })
|
|
});
|
|
|
|
if (res.ok) {
|
|
// Reload both snapshot lists
|
|
if (typeof loadSnapshots === 'function') loadSnapshots();
|
|
if (typeof loadVolumeSnapshots === 'function') loadVolumeSnapshots();
|
|
alert('Snapshot restored successfully');
|
|
} else {
|
|
const data = await res.json();
|
|
let errMsg = 'Failed to restore snapshot';
|
|
if (data) {
|
|
if (data.message) {
|
|
errMsg = data.message;
|
|
if (data.details) {
|
|
errMsg += ': ' + data.details;
|
|
}
|
|
} else if (data.error) {
|
|
errMsg = data.error;
|
|
}
|
|
}
|
|
alert(`Error: ${errMsg}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
async function deleteSnapshot(name) {
|
|
if (!confirm(`Are you sure you want to delete snapshot "${name}"?`)) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/snapshots/${encodeURIComponent(name)}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (res.ok) {
|
|
loadSnapshots();
|
|
alert('Snapshot deleted successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to delete snapshot'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Snapshot Policy Management
|
|
async function loadPolicies() {
|
|
try {
|
|
const res = await fetch('/api/v1/snapshot-policies', { headers: getAuthHeaders() });
|
|
const data = await res.json().catch(() => null);
|
|
const listEl = document.getElementById('policies-list');
|
|
|
|
if (!res.ok) {
|
|
const errorMsg = (data && data.error) ? data.error : `HTTP ${res.status}`;
|
|
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${errorMsg}</p>`;
|
|
return;
|
|
}
|
|
|
|
if (!data || !Array.isArray(data)) {
|
|
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
|
|
return;
|
|
}
|
|
|
|
if (data.length === 0) {
|
|
listEl.innerHTML = '<p class="text-slate-400 text-sm">No snapshot policies found. Create a policy to enable automatic snapshots.</p>';
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = data.map(policy => `
|
|
<div class="border-b border-slate-700 last:border-0 py-4">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<h3 class="text-lg font-semibold text-white">${policy.dataset}</h3>
|
|
${policy.autosnap ? '<span class="px-2 py-1 rounded text-xs font-medium bg-green-900 text-green-300">Auto-snap Enabled</span>' : '<span class="px-2 py-1 rounded text-xs font-medium bg-slate-700 text-slate-300">Auto-snap Disabled</span>'}
|
|
${policy.autoprune ? '<span class="px-2 py-1 rounded text-xs font-medium bg-blue-900 text-blue-300">Auto-prune Enabled</span>' : ''}
|
|
</div>
|
|
<div class="text-sm text-slate-400 space-y-1">
|
|
<p>Retention:
|
|
${policy.frequent > 0 ? `<span class="text-slate-300">${policy.frequent} frequent</span>` : ''}
|
|
${policy.hourly > 0 ? `<span class="text-slate-300">${policy.hourly} hourly</span>` : ''}
|
|
${policy.daily > 0 ? `<span class="text-slate-300">${policy.daily} daily</span>` : ''}
|
|
${policy.weekly > 0 ? `<span class="text-slate-300">${policy.weekly} weekly</span>` : ''}
|
|
${policy.monthly > 0 ? `<span class="text-slate-300">${policy.monthly} monthly</span>` : ''}
|
|
${policy.yearly > 0 ? `<span class="text-slate-300">${policy.yearly} yearly</span>` : ''}
|
|
${!policy.frequent && !policy.hourly && !policy.daily && !policy.weekly && !policy.monthly && !policy.yearly ? '<span class="text-slate-500">No retention configured</span>' : ''}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2 ml-4">
|
|
<button onclick="editPolicy('${policy.dataset}')" class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
|
|
Edit
|
|
</button>
|
|
<button onclick="deletePolicy('${policy.dataset}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (err) {
|
|
document.getElementById('policies-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function showCreatePolicyModal() {
|
|
await loadDatasetsForPolicy();
|
|
document.getElementById('create-policy-modal').classList.remove('hidden');
|
|
}
|
|
|
|
async function loadDatasetsForPolicy() {
|
|
try {
|
|
const res = await fetch('/api/v1/datasets', { headers: getAuthHeaders() });
|
|
const selectEl = document.getElementById('policy-dataset-select');
|
|
|
|
if (!res.ok) {
|
|
selectEl.innerHTML = '<option value="">Error loading datasets</option>';
|
|
return;
|
|
}
|
|
|
|
const datasets = await res.json();
|
|
|
|
if (!Array.isArray(datasets) || datasets.length === 0) {
|
|
selectEl.innerHTML = '<option value="">No datasets found</option>';
|
|
return;
|
|
}
|
|
|
|
selectEl.innerHTML = '<option value="">Select a dataset...</option>';
|
|
datasets.forEach(ds => {
|
|
const option = document.createElement('option');
|
|
option.value = ds.name;
|
|
option.textContent = `${ds.name} (${ds.type})`;
|
|
selectEl.appendChild(option);
|
|
});
|
|
} catch (err) {
|
|
console.error('Error loading datasets:', err);
|
|
document.getElementById('policy-dataset-select').innerHTML = '<option value="">Error loading datasets</option>';
|
|
}
|
|
}
|
|
|
|
async function createPolicy(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/snapshot-policies', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
dataset: formData.get('dataset'),
|
|
frequent: parseInt(formData.get('frequent')) || 0,
|
|
hourly: parseInt(formData.get('hourly')) || 0,
|
|
daily: parseInt(formData.get('daily')) || 0,
|
|
weekly: parseInt(formData.get('weekly')) || 0,
|
|
monthly: parseInt(formData.get('monthly')) || 0,
|
|
yearly: parseInt(formData.get('yearly')) || 0,
|
|
autosnap: formData.get('autosnap') === 'on',
|
|
autoprune: formData.get('autoprune') === 'on'
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeModal('create-policy-modal');
|
|
e.target.reset();
|
|
loadPolicies();
|
|
alert('Snapshot policy created successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to create snapshot policy'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
async function editPolicy(dataset) {
|
|
// Load policy and populate form (similar to create but with PUT)
|
|
alert('Edit policy feature - coming soon');
|
|
}
|
|
|
|
async function deletePolicy(dataset) {
|
|
if (!confirm(`Are you sure you want to delete snapshot policy for "${dataset}"?`)) return;
|
|
|
|
try {
|
|
const res = await fetch(`/api/v1/snapshot-policies/${encodeURIComponent(dataset)}`, {
|
|
method: 'DELETE',
|
|
headers: getAuthHeaders()
|
|
});
|
|
|
|
if (res.ok) {
|
|
loadPolicies();
|
|
alert('Snapshot policy deleted successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to delete snapshot policy'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Volume Snapshots (same as regular snapshots but filtered for volumes)
|
|
async function loadVolumeSnapshots() {
|
|
try {
|
|
const res = await fetch('/api/v1/snapshots', { headers: getAuthHeaders() });
|
|
const data = await res.json().catch(() => null);
|
|
const listEl = document.getElementById('volume-snapshots-list');
|
|
|
|
if (!res.ok) {
|
|
const errorMsg = (data && data.error) ? data.error : `HTTP ${res.status}`;
|
|
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${errorMsg}</p>`;
|
|
return;
|
|
}
|
|
|
|
if (!data || !Array.isArray(data)) {
|
|
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
|
|
return;
|
|
}
|
|
|
|
// Filter for volume snapshots (ZVOLs)
|
|
const volumeSnapshots = data.filter(snap => {
|
|
// Check if dataset is a volume (starts with pool/ and might be a zvol)
|
|
return snap.dataset && snap.dataset.includes('/');
|
|
});
|
|
|
|
if (volumeSnapshots.length === 0) {
|
|
listEl.innerHTML = '<p class="text-slate-400 text-sm">No volume snapshots found</p>';
|
|
return;
|
|
}
|
|
|
|
listEl.innerHTML = volumeSnapshots.map(snap => `
|
|
<div class="border-b border-slate-700 last:border-0 py-4">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<h3 class="text-lg font-semibold text-white font-mono">${snap.name}</h3>
|
|
</div>
|
|
<div class="text-sm text-slate-400 space-y-1">
|
|
<p>Volume: <span class="text-slate-300">${snap.dataset}</span></p>
|
|
<p>Size: <span class="text-slate-300">${formatBytes(snap.size || 0)}</span></p>
|
|
<p>Created: <span class="text-slate-300">${formatDate(snap.created_at)}</span></p>
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2 ml-4">
|
|
<button onclick="restoreSnapshot('${snap.name}', '${snap.dataset}')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
Restore
|
|
</button>
|
|
<button onclick="deleteSnapshot('${snap.name}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
|
|
Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
} catch (err) {
|
|
document.getElementById('volume-snapshots-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
|
|
}
|
|
}
|
|
|
|
async function showCreateVolumeSnapshotModal() {
|
|
await loadVolumesForSnapshot();
|
|
document.getElementById('create-volume-snapshot-modal').classList.remove('hidden');
|
|
}
|
|
|
|
async function loadVolumesForSnapshot() {
|
|
try {
|
|
const res = await fetch('/api/v1/zvols', { headers: getAuthHeaders() });
|
|
const selectEl = document.getElementById('volume-snapshot-select');
|
|
|
|
if (!res.ok) {
|
|
selectEl.innerHTML = '<option value="">Error loading volumes</option>';
|
|
return;
|
|
}
|
|
|
|
const volumes = await res.json();
|
|
|
|
if (!Array.isArray(volumes) || volumes.length === 0) {
|
|
selectEl.innerHTML = '<option value="">No volumes found</option>';
|
|
return;
|
|
}
|
|
|
|
selectEl.innerHTML = '<option value="">Select a volume...</option>';
|
|
volumes.forEach(vol => {
|
|
const option = document.createElement('option');
|
|
option.value = vol.name;
|
|
option.textContent = `${vol.name} (${formatBytes(vol.size)})`;
|
|
selectEl.appendChild(option);
|
|
});
|
|
} catch (err) {
|
|
console.error('Error loading volumes:', err);
|
|
document.getElementById('volume-snapshot-select').innerHTML = '<option value="">Error loading volumes</option>';
|
|
}
|
|
}
|
|
|
|
async function createVolumeSnapshot(e) {
|
|
e.preventDefault();
|
|
const formData = new FormData(e.target);
|
|
|
|
try {
|
|
const res = await fetch('/api/v1/snapshots', {
|
|
method: 'POST',
|
|
headers: getAuthHeaders(),
|
|
body: JSON.stringify({
|
|
dataset: formData.get('volume'),
|
|
name: formData.get('name'),
|
|
recursive: false
|
|
})
|
|
});
|
|
|
|
if (res.ok) {
|
|
closeModal('create-volume-snapshot-modal');
|
|
e.target.reset();
|
|
loadVolumeSnapshots();
|
|
alert('Volume snapshot created successfully');
|
|
} else {
|
|
const err = await res.json();
|
|
alert(`Error: ${err.error || 'Failed to create volume snapshot'}`);
|
|
}
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Replication (placeholder)
|
|
async function loadReplications() {
|
|
// Placeholder - replication not yet implemented
|
|
document.getElementById('replications-list').innerHTML = `
|
|
<div class="text-center py-8">
|
|
<p class="text-slate-400 text-sm mb-2">Replication feature coming soon</p>
|
|
<p class="text-slate-500 text-xs">This feature will allow you to replicate snapshots to remote systems</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function showCreateReplicationModal() {
|
|
alert('Replication feature coming soon');
|
|
}
|
|
|
|
// Load initial data
|
|
loadSnapshots();
|
|
</script>
|
|
{{end}}
|
|
|
|
{{define "protection.html"}}
|
|
{{template "base" .}}
|
|
{{end}}
|