Files
atlas/web/templates/storage.html

726 lines
26 KiB
HTML

{{define "storage-content"}}
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-white mb-2">Storage Management</h1>
<p class="text-slate-400">Manage storage pools, datasets, and volumes</p>
</div>
<div class="flex gap-2">
<button onclick="showCreatePoolModal()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium">
Create Pool
</button>
<button onclick="showImportPoolModal()" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg text-sm font-medium">
Import Pool
</button>
</div>
</div>
<!-- Tabs -->
<div class="border-b border-slate-800">
<nav class="flex gap-4">
<button onclick="switchTab('pools')" id="tab-pools" class="tab-button px-4 py-2 border-b-2 border-blue-600 text-blue-400 font-medium">
Pools
</button>
<button onclick="switchTab('datasets')" id="tab-datasets" class="tab-button px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300">
Datasets
</button>
<button onclick="switchTab('zvols')" id="tab-zvols" class="tab-button px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300">
Volumes
</button>
<button onclick="switchTab('disks')" id="tab-disks" class="tab-button px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300">
Disks
</button>
</nav>
</div>
<!-- Pools Tab -->
<div id="content-pools" 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">Storage Pools</h2>
<button onclick="loadPools()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
</div>
<div id="pools-list" class="p-4">
<p class="text-slate-400 text-sm">Loading...</p>
</div>
</div>
</div>
<!-- Datasets Tab -->
<div id="content-datasets" 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">Datasets</h2>
<div class="flex gap-2">
<button onclick="showCreateDatasetModal()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
Create Dataset
</button>
<button onclick="loadDatasets()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
</div>
</div>
<div id="datasets-list" class="p-4">
<p class="text-slate-400 text-sm">Loading...</p>
</div>
</div>
</div>
<!-- Storage Volumes Tab -->
<div id="content-zvols" 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">Storage Volumes</h2>
<div class="flex gap-2">
<button onclick="showCreateZVOLModal()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
Create Volume
</button>
<button onclick="loadZVOLs()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
</div>
</div>
<div id="zvols-list" class="p-4">
<p class="text-slate-400 text-sm">Loading...</p>
</div>
</div>
</div>
<!-- Disks Tab -->
<div id="content-disks" 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">Available Disks</h2>
<button onclick="loadDisks()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
</div>
<div id="disks-list" class="p-4">
<p class="text-slate-400 text-sm">Loading...</p>
</div>
</div>
</div>
</div>
<!-- Create Pool Modal -->
<div id="create-pool-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 Storage Pool</h3>
<form id="create-pool-form" onsubmit="createPool(event)" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Pool Name</label>
<input type="text" name="name" 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="block text-sm font-medium text-slate-300 mb-1">VDEVs (comma-separated)</label>
<input type="text" name="vdevs" placeholder="/dev/sdb,/dev/sdc" 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">
<p class="text-xs text-slate-400 mt-1">Enter device paths separated by commas</p>
</div>
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('create-pool-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>
<!-- Import Pool Modal -->
<div id="import-pool-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">Import Storage Pool</h3>
<form id="import-pool-form" onsubmit="importPool(event)" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Pool Name</label>
<select name="name" id="import-pool-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 available pools...</option>
</select>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" name="readonly" id="import-readonly" class="w-4 h-4 text-blue-600 bg-slate-900 border-slate-700 rounded">
<label for="import-readonly" class="text-sm text-slate-300">Import as read-only</label>
</div>
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('import-pool-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">
Import
</button>
</div>
</form>
</div>
</div>
<!-- Create Dataset Modal -->
<div id="create-dataset-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 Dataset</h3>
<form id="create-dataset-form" onsubmit="createDataset(event)" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Dataset Name</label>
<input type="text" name="name" placeholder="pool/dataset" 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="block text-sm font-medium text-slate-300 mb-1">Quota (optional)</label>
<input type="text" name="quota" placeholder="10G" 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">Compression (optional)</label>
<select name="compression" 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="">None</option>
<option value="lz4">lz4</option>
<option value="zstd">zstd</option>
<option value="gzip">gzip</option>
</select>
</div>
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('create-dataset-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 Storage Volume Modal -->
<div id="create-zvol-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 Storage Volume</h3>
<form id="create-zvol-form" onsubmit="createZVOL(event)" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Volume Name</label>
<input type="text" name="name" placeholder="pool/zvol" 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="block text-sm font-medium text-slate-300 mb-1">Size</label>
<input type="text" name="size" placeholder="10G" 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-zvol-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>
let currentTab = 'pools';
function switchTab(tab) {
currentTab = tab;
// Update tab buttons
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('border-blue-600', 'text-blue-400');
btn.classList.add('border-transparent', 'text-slate-400');
});
document.getElementById(`tab-${tab}`).classList.remove('border-transparent', 'text-slate-400');
document.getElementById(`tab-${tab}`).classList.add('border-blue-600', 'text-blue-400');
// Update content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.add('hidden');
});
document.getElementById(`content-${tab}`).classList.remove('hidden');
// Load data for the tab
if (tab === 'pools') loadPools();
else if (tab === 'datasets') loadDatasets();
else if (tab === 'zvols') loadZVOLs();
else if (tab === 'disks') loadDisks();
}
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 getAuthHeaders() {
const token = localStorage.getItem('atlas_token');
const headers = {
'Content-Type': 'application/json'
};
// Only add Authorization header if token exists (for mutations)
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
}
async function loadPools() {
try {
const res = await fetch('/api/v1/pools', { headers: getAuthHeaders() });
const data = await res.json().catch(() => null);
const listEl = document.getElementById('pools-list');
// Handle HTTP errors
if (!res.ok) {
const errorMsg = (data && data.error) ? data.error : `HTTP ${res.status}: Failed to load pools`;
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${errorMsg}</p>`;
return;
}
// Handle invalid response format
if (!data) {
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response (no data)</p>';
return;
}
// Check if data is an array
if (!Array.isArray(data)) {
// Log the actual response for debugging
console.error('Invalid response format:', data);
const errorMsg = (data.error) ? data.error : 'Invalid response format: expected array';
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${errorMsg}</p>`;
return;
}
const pools = data;
if (pools.length === 0) {
listEl.innerHTML = '<p class="text-slate-400 text-sm">No pools found. Create a pool to get started.</p>';
return;
}
listEl.innerHTML = pools.map(pool => `
<div class="border-b border-slate-700 last:border-0 py-4">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-lg font-semibold text-white">${pool.name}</h3>
<span class="px-2 py-1 rounded text-xs font-medium ${
pool.health === 'ONLINE' ? 'bg-green-900 text-green-300' :
pool.health === 'DEGRADED' ? 'bg-yellow-900 text-yellow-300' :
'bg-red-900 text-red-300'
}">${pool.health}</span>
</div>
<div class="grid grid-cols-3 gap-4 text-sm">
<div>
<span class="text-slate-400">Size:</span>
<span class="text-white ml-2">${formatBytes(pool.size)}</span>
</div>
<div>
<span class="text-slate-400">Used:</span>
<span class="text-white ml-2">${formatBytes(pool.allocated)}</span>
</div>
<div>
<span class="text-slate-400">Free:</span>
<span class="text-white ml-2">${formatBytes(pool.free)}</span>
</div>
</div>
</div>
<div class="flex gap-2">
<button onclick="startScrub('${pool.name}')" class="px-3 py-1.5 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
Scrub
</button>
<button onclick="exportPool('${pool.name}')" class="px-3 py-1.5 bg-yellow-600 hover:bg-yellow-700 text-white rounded text-sm">
Export
</button>
<button onclick="deletePool('${pool.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('pools-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
}
}
async function loadDatasets() {
try {
const res = await fetch('/api/v1/datasets', { headers: getAuthHeaders() });
if (!res.ok) {
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
document.getElementById('datasets-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.error || 'Failed to load datasets'}</p>`;
return;
}
const datasets = await res.json();
const listEl = document.getElementById('datasets-list');
if (!Array.isArray(datasets)) {
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
return;
}
if (datasets.length === 0) {
listEl.innerHTML = '<p class="text-slate-400 text-sm">No datasets found</p>';
return;
}
listEl.innerHTML = datasets.map(ds => `
<div class="border-b border-slate-700 last:border-0 py-4">
<div class="flex items-center justify-between">
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">${ds.name}</h3>
${ds.mountpoint ? `<p class="text-sm text-slate-400">${ds.mountpoint}</p>` : ''}
</div>
<button onclick="deleteDataset('${ds.name}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
Delete
</button>
</div>
</div>
`).join('');
} catch (err) {
document.getElementById('datasets-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
}
}
async function loadZVOLs() {
try {
const res = await fetch('/api/v1/zvols', { headers: getAuthHeaders() });
if (!res.ok) {
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
document.getElementById('zvols-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.error || 'Failed to load volumes'}</p>`;
return;
}
const zvols = await res.json();
const listEl = document.getElementById('zvols-list');
if (!Array.isArray(zvols)) {
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
return;
}
if (zvols.length === 0) {
listEl.innerHTML = '<p class="text-slate-400 text-sm">No volumes found</p>';
return;
}
listEl.innerHTML = zvols.map(zvol => `
<div class="border-b border-slate-700 last:border-0 py-4">
<div class="flex items-center justify-between">
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">${zvol.name}</h3>
<p class="text-sm text-slate-400">Size: ${formatBytes(zvol.size)}</p>
</div>
<button onclick="deleteZVOL('${zvol.name}')" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
Delete
</button>
</div>
</div>
`).join('');
} catch (err) {
document.getElementById('zvols-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
}
}
async function loadDisks() {
try {
const res = await fetch('/api/v1/disks', { headers: getAuthHeaders() });
if (!res.ok) {
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
document.getElementById('disks-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.error || 'Failed to load disks'}</p>`;
return;
}
const disks = await res.json();
const listEl = document.getElementById('disks-list');
if (!Array.isArray(disks)) {
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
return;
}
if (disks.length === 0) {
listEl.innerHTML = '<p class="text-slate-400 text-sm">No disks found</p>';
return;
}
listEl.innerHTML = disks.map(disk => `
<div class="border-b border-slate-700 last:border-0 py-4">
<div class="flex items-center justify-between">
<div class="flex-1">
<h3 class="text-lg font-semibold text-white mb-1">${disk.name}</h3>
<div class="text-sm text-slate-400 space-y-1">
${disk.size ? `<p>Size: ${disk.size}</p>` : ''}
${disk.model ? `<p>Model: ${disk.model}</p>` : ''}
</div>
</div>
</div>
</div>
`).join('');
} catch (err) {
document.getElementById('disks-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
}
}
function showCreatePoolModal() {
document.getElementById('create-pool-modal').classList.remove('hidden');
}
function showImportPoolModal() {
document.getElementById('import-pool-modal').classList.remove('hidden');
loadAvailablePools();
}
function showCreateDatasetModal() {
document.getElementById('create-dataset-modal').classList.remove('hidden');
}
function showCreateZVOLModal() {
document.getElementById('create-zvol-modal').classList.remove('hidden');
}
function closeModal(modalId) {
document.getElementById(modalId).classList.add('hidden');
}
async function loadAvailablePools() {
try {
const res = await fetch('/api/v1/pools/available', { headers: getAuthHeaders() });
const data = await res.json();
const select = document.getElementById('import-pool-select');
select.innerHTML = '<option value="">Select a pool...</option>';
if (data.pools && data.pools.length > 0) {
data.pools.forEach(pool => {
const option = document.createElement('option');
option.value = pool;
option.textContent = pool;
select.appendChild(option);
});
} else {
select.innerHTML = '<option value="">No pools available for import</option>';
}
} catch (err) {
console.error('Error loading available pools:', err);
}
}
async function createPool(e) {
e.preventDefault();
const formData = new FormData(e.target);
const vdevs = formData.get('vdevs').split(',').map(v => v.trim()).filter(v => v);
try {
const res = await fetch('/api/v1/pools', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
name: formData.get('name'),
vdevs: vdevs
})
});
if (res.ok) {
closeModal('create-pool-modal');
e.target.reset();
loadPools();
alert('Pool created successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to create pool'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function importPool(e) {
e.preventDefault();
const formData = new FormData(e.target);
const options = {};
if (formData.get('readonly')) {
options.readonly = 'on';
}
try {
const res = await fetch('/api/v1/pools/import', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
name: formData.get('name'),
options: options
})
});
if (res.ok) {
closeModal('import-pool-modal');
e.target.reset();
loadPools();
alert('Pool imported successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to import pool'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function createDataset(e) {
e.preventDefault();
const formData = new FormData(e.target);
const data = { name: formData.get('name') };
if (formData.get('quota')) data.quota = formData.get('quota');
if (formData.get('compression')) data.compression = formData.get('compression');
try {
const res = await fetch('/api/v1/datasets', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data)
});
if (res.ok) {
closeModal('create-dataset-modal');
e.target.reset();
loadDatasets();
alert('Dataset created successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to create dataset'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function createZVOL(e) {
e.preventDefault();
const formData = new FormData(e.target);
try {
const res = await fetch('/api/v1/zvols', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
name: formData.get('name'),
size: formData.get('size')
})
});
if (res.ok) {
closeModal('create-zvol-modal');
e.target.reset();
loadZVOLs();
alert('Storage volume created successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to create storage volume'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function deletePool(name) {
if (!confirm(`Are you sure you want to delete pool "${name}"? This will destroy all data!`)) return;
try {
const res = await fetch(`/api/v1/pools/${name}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (res.ok) {
loadPools();
alert('Pool deleted successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to delete pool'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function deleteDataset(name) {
if (!confirm(`Are you sure you want to delete dataset "${name}"?`)) return;
try {
const res = await fetch(`/api/v1/datasets/${encodeURIComponent(name)}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (res.ok) {
loadDatasets();
alert('Dataset deleted successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to delete dataset'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function deleteZVOL(name) {
if (!confirm(`Are you sure you want to delete storage volume "${name}"?`)) return;
try {
const res = await fetch(`/api/v1/zvols/${encodeURIComponent(name)}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (res.ok) {
loadZVOLs();
alert('Storage volume deleted successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to delete storage volume'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function startScrub(name) {
try {
const res = await fetch(`/api/v1/pools/${name}/scrub`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({})
});
if (res.ok) {
alert('Scrub started successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to start scrub'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function exportPool(name) {
if (!confirm(`Are you sure you want to export pool "${name}"?`)) return;
try {
const res = await fetch(`/api/v1/pools/${name}/export`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ force: false })
});
if (res.ok) {
loadPools();
alert('Pool exported successfully');
} else {
const err = await res.json();
alert(`Error: ${err.error || 'Failed to export pool'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
// Load initial data
loadPools();
</script>
{{end}}
{{define "storage.html"}}
{{template "base" .}}
{{end}}