fixing UI and iscsi sync
Some checks failed
CI / test-build (push) Has been cancelled

This commit is contained in:
2025-12-20 19:16:50 +00:00
parent 2bb892dfdc
commit 6202ef8e83
12 changed files with 1436 additions and 116 deletions

View File

@@ -233,6 +233,35 @@
</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">
@@ -435,8 +464,10 @@ async function loadPools() {
return;
}
console.log('Rendering pools list...');
listEl.innerHTML = pools.map(pool => `
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">
@@ -462,8 +493,22 @@ async function loadPools() {
<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>
@@ -476,7 +521,14 @@ async function loadPools() {
</div>
</div>
</div>
`).join('');
`;
}).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);
@@ -1069,6 +1121,197 @@ async function createZVOL(e) {
}
}
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;