Files
atlas/web/templates/storage.html
Othman Hendy Suseo 6202ef8e83
Some checks failed
CI / test-build (push) Has been cancelled
fixing UI and iscsi sync
2025-12-20 19:16:50 +00:00

1465 lines
58 KiB
HTML

{{define "storage-content"}}
<div class="space-y-4 sm:space-y-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 class="text-2xl sm:text-3xl font-bold text-white mb-2">Storage Management</h1>
<p class="text-sm sm:text-base text-slate-400">Manage storage pools, datasets, and volumes</p>
</div>
<div class="flex flex-col sm:flex-row 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 transition-colors">
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 transition-colors">
Import Pool
</button>
</div>
</div>
<!-- Tabs -->
<div class="border-b border-slate-800 overflow-x-auto">
<nav class="flex gap-2 sm:gap-4 min-w-max">
<button onclick="switchTab('pools')" id="tab-pools" class="tab-button px-3 sm:px-4 py-2 border-b-2 border-blue-600 text-blue-400 font-medium whitespace-nowrap text-sm sm:text-base">
Pools
</button>
<button onclick="switchTab('datasets')" id="tab-datasets" class="tab-button px-3 sm:px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300 whitespace-nowrap text-sm sm:text-base">
Datasets
</button>
<button onclick="switchTab('zvols')" id="tab-zvols" class="tab-button px-3 sm:px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300 whitespace-nowrap text-sm sm:text-base">
Volumes
</button>
<button onclick="switchTab('disks')" id="tab-disks" class="tab-button px-3 sm:px-4 py-2 border-b-2 border-transparent text-slate-400 hover:text-slate-300 whitespace-nowrap text-sm sm:text-base">
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="space-y-6">
<!-- System Overview Card -->
<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">
<div>
<h2 class="text-lg font-semibold text-white">System - Available Disks</h2>
<p class="text-sm text-slate-400 mt-1" id="disks-summary">Loading disk information...</p>
</div>
<button onclick="loadDisks()" class="text-sm text-slate-400 hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
</button>
</div>
<div class="p-6">
<!-- Visual Disk Grid -->
<div id="disks-visual" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-3 sm:gap-4 mb-6">
<p class="text-slate-400 text-sm col-span-full">Loading...</p>
</div>
<!-- Legend -->
<div class="border-t border-slate-700 pt-4">
<h3 class="text-sm font-medium text-slate-300 mb-3">Status Legend</h3>
<div class="flex flex-wrap gap-4 text-xs">
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded bg-slate-700 border border-slate-500"></div>
<span class="text-slate-400">Available</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded bg-blue-600 border border-blue-500"></div>
<span class="text-slate-400">In Use</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded bg-slate-600 border border-slate-500"></div>
<span class="text-slate-400">Free</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded bg-yellow-600 border border-yellow-500"></div>
<span class="text-slate-400">Warning</span>
</div>
<div class="flex items-center gap-2">
<div class="w-4 h-4 rounded bg-red-600 border border-red-500"></div>
<span class="text-slate-400">Error</span>
</div>
</div>
</div>
</div>
</div>
<!-- Detailed Disk List -->
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
<div class="p-4 border-b border-slate-700">
<h2 class="text-lg font-semibold text-white">Disk Details</h2>
</div>
<div id="disks-list" class="p-4">
<p class="text-slate-400 text-sm">Loading...</p>
</div>
</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 p-4">
<div class="bg-slate-800 rounded-lg border border-slate-700 p-4 sm:p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<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 p-4">
<div class="bg-slate-800 rounded-lg border border-slate-700 p-4 sm:p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<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 p-4">
<div class="bg-slate-800 rounded-lg border border-slate-700 p-4 sm:p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<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>
<!-- Add Spare Disk Modal -->
<div id="add-spare-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div class="bg-slate-800 rounded-lg border border-slate-700 p-4 sm:p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<h3 class="text-xl font-semibold text-white mb-4">Add Spare Disk</h3>
<form id="add-spare-form" onsubmit="addSpareDisk(event)" class="space-y-4">
<input type="hidden" id="add-spare-pool-name" name="pool">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Pool Name</label>
<input type="text" id="add-spare-pool-name-display" readonly class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm opacity-75 cursor-not-allowed">
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Select Disk</label>
<select id="add-spare-disk" name="disk" 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 disks...</option>
</select>
<p class="text-xs text-slate-400 mt-1">Only available disks are shown</p>
</div>
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('add-spare-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-green-600 hover:bg-green-700 text-white rounded text-sm">
Add Spare
</button>
</div>
</form>
</div>
</div>
<!-- Edit Dataset Modal -->
<div id="edit-dataset-modal" class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div class="bg-slate-800 rounded-lg border border-slate-700 p-4 sm:p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<h3 class="text-xl font-semibold text-white mb-4">Edit Dataset</h3>
<form id="edit-dataset-form" onsubmit="updateDataset(event)" class="space-y-4">
<input type="hidden" id="edit-dataset-name" name="name">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Dataset Name</label>
<input type="text" id="edit-dataset-name-display" readonly class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm opacity-75 cursor-not-allowed">
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Quota (optional)</label>
<input type="text" id="edit-dataset-quota" name="quota" placeholder="10G, 1T, or 'none' to remove" 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">Leave empty to keep current quota. Use 'none' to remove quota.</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Compression (optional)</label>
<select id="edit-dataset-compression" 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="">Keep current</option>
<option value="off">off</option>
<option value="lz4">lz4</option>
<option value="zstd">zstd</option>
<option value="gzip">gzip</option>
<option value="gzip-1">gzip-1</option>
<option value="gzip-9">gzip-9</option>
</select>
</div>
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('edit-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">
Update
</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 p-4">
<div class="bg-slate-800 rounded-lg border border-slate-700 p-4 sm:p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
<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>
// 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;
}
})();
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 listEl = document.getElementById('pools-list');
if (!listEl) {
console.error('pools-list element not found');
return;
}
// Add cache-busting to ensure fresh data
const authHeaders = getAuthHeaders();
const headers = {
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
};
// Merge auth headers
Object.assign(headers, authHeaders);
const res = await fetch('/api/v1/pools?_=' + Date.now(), {
headers: headers
});
console.log('API response status:', res.status, res.statusText);
let data = null;
try {
const text = await res.text();
console.log('API response text:', text.substring(0, 200));
data = JSON.parse(text);
console.log('Parsed data:', data);
} catch (jsonErr) {
console.error('JSON parse error:', jsonErr);
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Failed to parse response</p>';
return;
}
// Handle HTTP errors
if (!res.ok) {
let errorMsg = `HTTP ${res.status}: Failed to load pools`;
if (data) {
if (data.message) {
errorMsg = data.message;
if (data.details) errorMsg += ': ' + data.details;
} else if (data.error) {
errorMsg = data.error;
if (data.details) errorMsg += ': ' + data.details;
}
}
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);
let errorMsg = 'Invalid response format: expected array';
if (data.message) {
errorMsg = data.message;
if (data.details) errorMsg += ': ' + data.details;
} else if (data.error) {
errorMsg = data.error;
if (data.details) errorMsg += ': ' + data.details;
}
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${errorMsg}</p>`;
return;
}
const pools = data;
console.log('Pools array:', pools, 'Length:', pools.length);
if (pools.length === 0) {
console.log('No pools found, showing empty message');
listEl.innerHTML = '<p class="text-slate-400 text-sm">No pools found. Create a pool to get started.</p>';
return;
}
console.log('Rendering pools list...', pools);
const html = pools.map(pool => {
console.log('Rendering pool:', pool.name);
return `
<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 class="mt-3">
<button onclick="togglePoolInfo('${pool.name}')" class="text-sm text-blue-400 hover:text-blue-300 flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
Pool Info
</button>
</div>
<div id="pool-info-${pool.name}" class="hidden mt-4 p-4 bg-slate-900 rounded border border-slate-700">
<p class="text-slate-400 text-sm">Loading pool details...</p>
</div>
</div>
<div class="flex gap-2">
<button onclick="showAddSpareModal('${pool.name}')" class="px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white rounded text-sm">
Add Spare
</button>
<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('');
console.log('Generated HTML contains Pool Info:', html.includes('Pool Info'));
console.log('Generated HTML contains togglePoolInfo:', html.includes('togglePoolInfo'));
console.log('Generated HTML length:', html.length);
listEl.innerHTML = html;
console.log('Pools list rendered successfully');
} catch (err) {
console.error('Error in loadPools:', err);
const listEl = document.getElementById('pools-list');
if (listEl) {
listEl.innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message || 'Failed to load pools'}</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 class="text-xs text-slate-500 mt-1">
Used: ${formatBytes(ds.used || 0)} | Available: ${formatBytes(ds.available || 0)}
</div>
</div>
<div class="flex gap-2">
<button onclick="showEditDatasetModal('${ds.name}')" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
Edit
</button>
<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>
</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>`;
document.getElementById('disks-visual').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');
const visualEl = document.getElementById('disks-visual');
const summaryEl = document.getElementById('disks-summary');
if (!Array.isArray(disks)) {
listEl.innerHTML = '<p class="text-red-400 text-sm">Error: Invalid response format</p>';
visualEl.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>';
visualEl.innerHTML = '<p class="text-slate-400 text-sm">No disks found</p>';
summaryEl.textContent = 'No disks available';
return;
}
// Update summary
summaryEl.textContent = `Disk: ${disks.length}`;
// Visual disk blocks (like QNAP) - using slate theme with numbered slots
visualEl.innerHTML = disks.map((disk, index) => {
const slotNumber = index + 1;
const isAvailable = disk.status === 'available';
// Set colors based on availability
const statusColor = isAvailable ? 'bg-slate-700' : 'bg-slate-800';
const statusBorder = isAvailable ? 'border-slate-500' : 'border-slate-600';
const statusIndicator = isAvailable ? 'bg-green-500' : 'bg-red-500';
const statusText = isAvailable ? 'Available' : 'Unavailable';
const cursorClass = isAvailable ? 'cursor-pointer' : 'cursor-not-allowed opacity-75';
const hoverClass = isAvailable ? 'hover:scale-105 hover:shadow-xl hover:shadow-blue-500/20 hover:-translate-y-1 hover:border-blue-500' : '';
return `
<div class="group relative">
<div class="disk-block ${statusColor} ${statusBorder} border-2 rounded-xl p-4 min-w-[140px] ${cursorClass} transition-all duration-200 ${hoverClass}"
title="Slot ${slotNumber}\n${disk.name}\n${disk.size || 'Unknown size'}\n${disk.path || `/dev/${disk.name}`}\nStatus: ${statusText}"
${isAvailable ? `onclick="selectDisk('${disk.name}')"` : ''}>
<!-- Slot Number Badge -->
<div class="absolute -top-2 -left-2 w-8 h-8 rounded-full ${isAvailable ? 'bg-blue-600' : 'bg-slate-600'} border-2 border-slate-800 flex items-center justify-center text-white font-bold text-xs shadow-lg z-10">
${slotNumber}
</div>
<!-- Disk Icon Container -->
<div class="flex flex-col items-center justify-center pt-2">
<div class="w-20 h-20 rounded-xl bg-gradient-to-br ${isAvailable ? 'from-slate-600 to-slate-700' : 'from-slate-700 to-slate-800'} flex items-center justify-center mb-3 border-2 border-slate-500/50 ${isAvailable ? 'group-hover:border-blue-500/70 group-hover:from-blue-600/30 group-hover:to-slate-700' : ''} transition-all shadow-inner">
<!-- Disk Icon -->
${isAvailable ? `
<svg class="w-12 h-12 text-slate-200 group-hover:text-blue-400 transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path d="M4 3a2 2 0 100 4h12a2 2 0 100-4H4z"></path>
<path fill-rule="evenodd" d="M3 8h14v7a2 2 0 01-2 2H5a2 2 0 01-2-2V8zm5 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" clip-rule="evenodd"></path>
</svg>
` : `
<svg class="w-12 h-12 text-red-400 transition-colors" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z" clip-rule="evenodd"></path>
</svg>
`}
</div>
<!-- Device Name -->
<div class="text-center w-full">
<div class="${isAvailable ? 'text-slate-100 group-hover:text-blue-400' : 'text-slate-400'} font-bold text-base mb-1 transition-colors font-mono">
${disk.name}
</div>
${disk.health_status ? `
<div class="mb-1">
<span class="px-1.5 py-0.5 rounded text-xs font-medium ${
disk.health_status === 'healthy' ? 'bg-green-600 text-white' :
disk.health_status === 'failed' ? 'bg-red-600 text-white' :
'bg-yellow-600 text-white'
}">${disk.health_status === 'healthy' ? '✓ Healthy' : disk.health_status === 'failed' ? '✗ Failed' : '? Unknown'}</span>
</div>
` : ''}
<div class="${isAvailable ? 'text-slate-300' : 'text-slate-500'} text-xs font-semibold mb-1">
${disk.size || 'N/A'}
</div>
<div class="${isAvailable ? 'text-slate-400' : 'text-slate-600'} text-xs font-mono">
${disk.path || `/dev/${disk.name}`}
</div>
<div class="text-xs font-semibold mt-1 ${isAvailable ? 'text-green-400' : 'text-red-400'}">
${statusText}
</div>
</div>
</div>
<!-- Status indicator -->
<div class="absolute top-2 right-2">
<div class="w-3 h-3 ${statusIndicator} rounded-full shadow-md border-2 border-slate-800 ${isAvailable ? 'animate-pulse' : ''}"></div>
</div>
</div>
</div>
`;
}).join('');
// Detailed disk list with slot numbers
listEl.innerHTML = disks.map((disk, index) => {
const isAvailable = disk.status === 'available';
const status = isAvailable ? 'Available' : 'Unavailable';
const statusColor = isAvailable ? 'bg-green-600 text-white' : 'bg-red-600 text-white';
const diskPath = disk.path || `/dev/${disk.name}`;
const slotNumber = index + 1;
return `
<div class="border-b border-slate-700 last:border-0 py-4 hover:bg-slate-750 transition-colors" data-disk-name="${disk.name}">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 flex-1">
<!-- Slot Number Badge -->
<div class="w-12 h-12 rounded-lg bg-blue-600 border-2 border-slate-700 flex items-center justify-center flex-shrink-0 font-bold text-white text-lg shadow-lg">
${slotNumber}
</div>
<!-- Disk Icon -->
<div class="w-12 h-12 rounded-lg ${isAvailable ? 'bg-slate-700 border-slate-600' : 'bg-slate-800 border-slate-700 opacity-60'} border flex items-center justify-center flex-shrink-0">
${isAvailable ? `
<svg class="w-6 h-6 text-slate-300" fill="currentColor" viewBox="0 0 20 20">
<path d="M4 3a2 2 0 100 4h12a2 2 0 100-4H4z"></path>
<path fill-rule="evenodd" d="M3 8h14v7a2 2 0 01-2 2H5a2 2 0 01-2-2V8zm5 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" clip-rule="evenodd"></path>
</svg>
` : `
<svg class="w-6 h-6 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z" clip-rule="evenodd"></path>
</svg>
`}
</div>
<!-- Disk Info -->
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-lg font-semibold ${isAvailable ? 'text-white' : 'text-slate-400'} font-mono">${disk.name}</h3>
<span class="px-2 py-1 rounded text-xs font-medium ${statusColor}">${status}</span>
<span class="text-xs ${isAvailable ? 'text-slate-500' : 'text-slate-600'} font-mono">${diskPath}</span>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm">
${disk.size ? `
<div>
<span class="text-slate-400">Size:</span>
<span class="text-white ml-2 font-medium">${disk.size}</span>
</div>
` : ''}
${disk.model ? `
<div>
<span class="text-slate-400">Model:</span>
<span class="text-white ml-2">${disk.model}</span>
</div>
` : ''}
<div>
<span class="text-slate-400">Slot:</span>
<span class="text-white ml-2 font-semibold">#${slotNumber}</span>
</div>
${disk.health_status ? `
<div>
<span class="text-slate-400">Health:</span>
<span class="ml-2 px-2 py-0.5 rounded text-xs font-medium ${
disk.health_status === 'healthy' ? 'bg-green-600 text-white' :
disk.health_status === 'failed' ? 'bg-red-600 text-white' :
'bg-yellow-600 text-white'
}">${disk.health_status === 'healthy' ? 'Healthy' : disk.health_status === 'failed' ? 'Failed' : 'Unknown'}</span>
</div>
` : ''}
${disk.health_temperature ? `
<div>
<span class="text-slate-400">Temp:</span>
<span class="text-white ml-2">${disk.health_temperature}</span>
</div>
` : ''}
${disk.health_power_on_hours ? `
<div>
<span class="text-slate-400">Power On:</span>
<span class="text-white ml-2">${parseInt(disk.health_power_on_hours).toLocaleString()}h</span>
</div>
` : ''}
</div>
</div>
</div>
<!-- Actions -->
<div class="flex gap-2">
<button ${isAvailable ? `onclick="useDiskForPool('${diskPath}')"` : 'disabled'}
class="px-3 py-1.5 ${isAvailable ? 'bg-blue-600 hover:bg-blue-700 text-white cursor-pointer' : 'bg-slate-600 text-slate-400 cursor-not-allowed'} rounded text-sm transition-colors">
Use for Pool
</button>
</div>
</div>
</div>
`;
}).join('');
} catch (err) {
document.getElementById('disks-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
document.getElementById('disks-visual').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
}
}
function selectDisk(diskName) {
// Scroll to disk in detailed list
const diskElement = document.querySelector(`[data-disk-name="${diskName}"]`);
if (diskElement) {
diskElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
diskElement.classList.add('ring-2', 'ring-blue-500');
setTimeout(() => {
diskElement.classList.remove('ring-2', 'ring-blue-500');
}, 2000);
}
}
function useDiskForPool(diskPath) {
// Open create pool modal and pre-fill with this disk
showCreatePoolModal();
const vdevsInput = document.querySelector('#create-pool-form input[name="vdevs"]');
if (vdevsInput) {
const currentValue = vdevsInput.value.trim();
if (currentValue) {
vdevsInput.value = currentValue + ',' + diskPath;
} else {
vdevsInput.value = diskPath;
}
vdevsInput.focus();
}
}
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');
}
async function showEditDatasetModal(datasetName) {
// Load dataset details first
try {
const res = await fetch(`/api/v1/datasets/${encodeURIComponent(datasetName)}`, { headers: getAuthHeaders() });
if (!res.ok) {
alert('Failed to load dataset details');
return;
}
const dataset = await res.json();
// Populate form
document.getElementById('edit-dataset-name').value = dataset.name;
document.getElementById('edit-dataset-name-display').value = dataset.name;
// Get current quota and compression from dataset properties
// Note: These might not be in the response, so we'll leave them empty
document.getElementById('edit-dataset-quota').value = '';
document.getElementById('edit-dataset-compression').value = '';
// Show modal
document.getElementById('edit-dataset-modal').classList.remove('hidden');
} catch (err) {
alert(`Error: ${err.message}`);
}
}
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
})
});
const data = await res.json().catch(() => null);
// Always refresh pool list after creation attempt, regardless of response
// Wait a moment for pool creation to complete
await new Promise(resolve => setTimeout(resolve, 1000));
await loadPools();
if (res.ok) {
closeModal('create-pool-modal');
e.target.reset();
alert('Pool created successfully');
} else {
// Check if pool appears in the list (might have been created despite error)
// Handle both error formats: APIError {code, message, details} or simple {error, details}
let errMsg = 'Failed to create pool';
if (data) {
if (data.message) {
// APIError format
errMsg = data.message;
if (data.details) {
errMsg += ': ' + data.details;
}
} else if (data.error) {
// Simple error format
errMsg = data.error;
if (data.details) {
errMsg += ': ' + data.details;
}
}
}
alert(`Error: ${errMsg}\n\nNote: The pool list has been refreshed. Please check if the pool was created.`);
}
} catch (err) {
// On network error, still try to refresh to see if pool was created
await new Promise(resolve => setTimeout(resolve, 1000));
await loadPools();
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 data = await res.json();
let errMsg = 'Failed to create dataset';
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 updateDataset(e) {
e.preventDefault();
const formData = new FormData(e.target);
const datasetName = formData.get('name');
const data = {};
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/${encodeURIComponent(datasetName)}`, {
method: 'PUT',
headers: getAuthHeaders(),
body: JSON.stringify(data)
});
if (res.ok) {
closeModal('edit-dataset-modal');
e.target.reset();
loadDatasets();
alert('Dataset updated successfully');
} else {
const data = await res.json();
let errMsg = 'Failed to update dataset';
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 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 togglePoolInfo(poolName) {
const infoEl = document.getElementById(`pool-info-${poolName}`);
if (!infoEl) return;
if (infoEl.classList.contains('hidden')) {
infoEl.classList.remove('hidden');
await loadPoolInfo(poolName);
} else {
infoEl.classList.add('hidden');
}
}
async function loadPoolInfo(poolName) {
const infoEl = document.getElementById(`pool-info-${poolName}`);
if (!infoEl) return;
try {
const res = await fetch(`/api/v1/pools/${poolName}?detail=true`, {
headers: getAuthHeaders()
});
if (!res.ok) {
infoEl.innerHTML = '<p class="text-red-400 text-sm">Failed to load pool details</p>';
return;
}
const detail = await res.json();
let html = '<div class="space-y-4">';
// State and Status
html += `<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-slate-400">State:</span>
<span class="text-white ml-2 px-2 py-1 rounded text-xs font-medium ${
detail.state === 'ONLINE' ? 'bg-green-900 text-green-300' :
detail.state === 'DEGRADED' ? 'bg-yellow-900 text-yellow-300' :
'bg-red-900 text-red-300'
}">${detail.state || 'N/A'}</span>
</div>
<div>
<span class="text-slate-400">Errors:</span>
<span class="text-white ml-2">${detail.errors || 'No known data errors'}</span>
</div>
</div>`;
// Scrub Info
if (detail.scrub_info) {
html += `<div class="text-sm">
<span class="text-slate-400">Scrub:</span>
<span class="text-white ml-2">${detail.scrub_info}</span>
</div>`;
}
// VDEVs
if (detail.vdevs && detail.vdevs.length > 0) {
html += '<div><h4 class="text-sm font-semibold text-white mb-2">Virtual Devices:</h4>';
html += '<div class="space-y-3">';
detail.vdevs.forEach(vdev => {
html += `<div class="pl-4 border-l-2 border-slate-600">
<div class="flex items-center gap-2 mb-1">
<span class="text-sm font-medium text-slate-300">${vdev.name || vdev.type || 'VDEV'}</span>
<span class="px-2 py-0.5 rounded text-xs ${
vdev.state === 'ONLINE' ? 'bg-green-900 text-green-300' :
vdev.state === 'DEGRADED' ? 'bg-yellow-900 text-yellow-300' :
'bg-red-900 text-red-300'
}">${vdev.state}</span>
${vdev.read > 0 || vdev.write > 0 || vdev.checksum > 0 ? `
<span class="text-xs text-slate-400">
(R:${vdev.read} W:${vdev.write} C:${vdev.checksum})
</span>
` : ''}
</div>`;
if (vdev.disks && vdev.disks.length > 0) {
html += '<div class="ml-4 space-y-1">';
vdev.disks.forEach(disk => {
html += `<div class="text-xs flex items-center gap-2">
<span class="text-slate-300">${disk.name}</span>
<span class="px-1.5 py-0.5 rounded text-xs ${
disk.state === 'ONLINE' ? 'bg-green-900 text-green-300' :
disk.state === 'DEGRADED' ? 'bg-yellow-900 text-yellow-300' :
'bg-red-900 text-red-300'
}">${disk.state}</span>
${disk.read > 0 || disk.write > 0 || disk.checksum > 0 ? `
<span class="text-slate-500">
(R:${disk.read} W:${disk.write} C:${disk.checksum})
</span>
` : ''}
</div>`;
});
html += '</div>';
}
html += '</div>';
});
html += '</div></div>';
}
// Spares
if (detail.spares && detail.spares.length > 0) {
html += '<div><h4 class="text-sm font-semibold text-white mb-2">Spare Disks:</h4>';
html += '<div class="flex flex-wrap gap-2">';
detail.spares.forEach(spare => {
html += `<span class="px-2 py-1 bg-slate-700 text-slate-300 rounded text-xs">${spare}</span>`;
});
html += '</div></div>';
} else {
html += '<div><h4 class="text-sm font-semibold text-white mb-2">Spare Disks:</h4>';
html += '<p class="text-slate-400 text-sm">No spare disks configured</p></div>';
}
html += '</div>';
infoEl.innerHTML = html;
} catch (err) {
console.error('Error loading pool info:', err);
infoEl.innerHTML = '<p class="text-red-400 text-sm">Error loading pool details</p>';
}
}
function showAddSpareModal(poolName) {
document.getElementById('add-spare-pool-name').value = poolName;
document.getElementById('add-spare-pool-name-display').value = poolName;
document.getElementById('add-spare-modal').classList.remove('hidden');
loadDisksForSpare();
}
async function loadDisksForSpare() {
try {
const res = await fetch('/api/v1/disks', {
headers: getAuthHeaders()
});
if (!res.ok) return;
const disks = await res.json();
const selectEl = document.getElementById('add-spare-disk');
selectEl.innerHTML = '<option value="">Select a disk...</option>';
disks.forEach(disk => {
if (disk.status === 'available') {
const option = document.createElement('option');
option.value = disk.name;
option.textContent = `${disk.name} (${disk.size || 'Unknown'})`;
selectEl.appendChild(option);
}
});
} catch (err) {
console.error('Error loading disks:', err);
}
}
async function addSpareDisk(event) {
event.preventDefault();
const poolName = document.getElementById('add-spare-pool-name').value;
const diskName = document.getElementById('add-spare-disk').value;
if (!diskName) {
alert('Please select a disk');
return;
}
try {
const res = await fetch(`/api/v1/pools/${poolName}/spare`, {
method: 'POST',
headers: {
...getAuthHeaders(),
'Content-Type': 'application/json'
},
body: JSON.stringify({ disk: diskName })
});
if (res.ok) {
closeModal('add-spare-modal');
loadPools();
alert('Spare disk added successfully');
} else {
const data = await res.json();
let errMsg = 'Failed to add spare disk';
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 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) {
// Wait a moment for the deletion to fully complete, then refresh
await new Promise(resolve => setTimeout(resolve, 500));
await 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().catch(() => null);
let errMsg = 'Failed to delete dataset';
if (err) {
if (err.message) {
errMsg = err.message;
if (err.details) errMsg += ': ' + err.details;
} else if (err.error) {
errMsg = err.error;
if (err.details) errMsg += ': ' + err.details;
}
}
alert(`Error: ${errMsg}`);
}
} 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().catch(() => null);
let errMsg = 'Failed to delete storage volume';
if (err) {
if (err.message) {
errMsg = err.message;
if (err.details) errMsg += ': ' + err.details;
} else if (err.error) {
errMsg = err.error;
if (err.details) errMsg += ': ' + err.details;
}
}
alert(`Error: ${errMsg}`);
}
} 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 when DOM is ready
(function() {
function initLoad() {
console.log('Initializing loadPools...');
try {
loadPools();
} catch (err) {
console.error('Error calling loadPools:', err);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initLoad);
} else {
// DOM is already ready
setTimeout(initLoad, 100); // Small delay to ensure DOM is fully ready
}
})();
</script>
{{end}}
{{define "storage.html"}}
{{template "base" .}}
{{end}}