|
|
|
|
@@ -76,7 +76,10 @@
|
|
|
|
|
<h2 class="text-lg font-semibold text-white">Virtual Tape Drives</h2>
|
|
|
|
|
<p class="text-xs text-slate-400 mt-1">Manage tape drives and loaded media</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button onclick="loadVTLDrives()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<button onclick="showCreateDriveModal()" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">Add Drive</button>
|
|
|
|
|
<button onclick="loadVTLDrives()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="vtl-drives-list" class="p-4">
|
|
|
|
|
<p class="text-slate-400 text-sm">Loading...</p>
|
|
|
|
|
@@ -112,7 +115,12 @@
|
|
|
|
|
<h2 class="text-lg font-semibold text-white">Media Changer</h2>
|
|
|
|
|
<p class="text-xs text-slate-400 mt-1">Manage tape library slots and operations</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button onclick="loadMediaChanger()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
<button onclick="showCreateChangerModal()" id="create-changer-btn" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
|
|
|
Add Media Changer
|
|
|
|
|
</button>
|
|
|
|
|
<button onclick="loadMediaChanger()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="vtl-changer-content" class="p-4">
|
|
|
|
|
<p class="text-slate-400 text-sm">Loading...</p>
|
|
|
|
|
@@ -228,6 +236,107 @@
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Create/Edit Media Changer Modal -->
|
|
|
|
|
<div id="changer-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 id="changer-modal-title" class="text-xl font-semibold text-white mb-4">Add Media Changer</h3>
|
|
|
|
|
<form id="changer-form" onsubmit="saveChanger(event)" class="space-y-4">
|
|
|
|
|
<input type="hidden" id="changer-library-id">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Library ID *</label>
|
|
|
|
|
<input type="number" id="changer-id-input" name="library_id" min="1" 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">Unique library identifier (e.g., 10, 20, 30)</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Vendor</label>
|
|
|
|
|
<input type="text" id="changer-vendor-input" name="vendor" placeholder="STK" 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">Default: STK</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Product</label>
|
|
|
|
|
<input type="text" id="changer-product-input" name="product" placeholder="L700 or L80" 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">Default: L700 (or L80 for library 30)</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Serial Number</label>
|
|
|
|
|
<input type="text" id="changer-serial-input" name="serial" placeholder="Auto-generated" 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 for auto-generation</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="changer-slots-div">
|
|
|
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Number of Slots</label>
|
|
|
|
|
<input type="number" id="changer-slots-input" name="num_slots" min="1" value="10" 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">Default: 10 slots</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex gap-2 justify-end">
|
|
|
|
|
<button type="button" onclick="closeModal('changer-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">
|
|
|
|
|
Save
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Create/Edit Drive Modal -->
|
|
|
|
|
<div id="drive-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 id="drive-modal-title" class="text-xl font-semibold text-white mb-4">Add Drive</h3>
|
|
|
|
|
<form id="drive-form" onsubmit="saveDrive(event)" class="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Drive ID *</label>
|
|
|
|
|
<input type="number" id="drive-id-input" name="drive_id" min="1" 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">Unique drive identifier (e.g., 11, 12, 21)</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Library ID *</label>
|
|
|
|
|
<input type="number" id="drive-library-id-input" name="library_id" min="1" 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">Library where this drive belongs</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Slot ID *</label>
|
|
|
|
|
<input type="number" id="drive-slot-id-input" name="slot_id" min="1" 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">Slot number in the library</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Vendor</label>
|
|
|
|
|
<select id="drive-vendor-input" name="vendor" onchange="updateDriveProductOptions()" 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="IBM">IBM</option>
|
|
|
|
|
<option value="HP">HP</option>
|
|
|
|
|
<option value="Quantum">Quantum</option>
|
|
|
|
|
<option value="Tandberg">Tandberg</option>
|
|
|
|
|
<option value="Overland">Overland</option>
|
|
|
|
|
</select>
|
|
|
|
|
<p class="text-xs text-slate-400 mt-1">Select drive vendor</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Product (LTO Drive Type)</label>
|
|
|
|
|
<select id="drive-product-input" name="product" 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="ULT3580-TD5">IBM ULT3580-TD5 (LTO-5)</option>
|
|
|
|
|
<option value="ULT3580-TD6">IBM ULT3580-TD6 (LTO-6)</option>
|
|
|
|
|
<option value="ULT3580-TD7">IBM ULT3580-TD7 (LTO-7)</option>
|
|
|
|
|
<option value="ULT3580-TD8">IBM ULT3580-TD8 (LTO-8)</option>
|
|
|
|
|
</select>
|
|
|
|
|
<p class="text-xs text-slate-400 mt-1">Select LTO drive generation</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label class="block text-sm font-medium text-slate-300 mb-1">Serial Number</label>
|
|
|
|
|
<input type="text" id="drive-serial-input" name="serial" placeholder="Auto-generated" 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 for auto-generation</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex gap-2 justify-end">
|
|
|
|
|
<button type="button" onclick="closeModal('drive-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">
|
|
|
|
|
Save
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Service Control Modal -->
|
|
|
|
|
<div id="service-control-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">
|
|
|
|
|
@@ -263,8 +372,49 @@
|
|
|
|
|
window.location.href = '/login?return=' + encodeURIComponent(window.location.pathname);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hide create/update/delete buttons for viewer role
|
|
|
|
|
hideButtonsForViewer();
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
// Get current user role
|
|
|
|
|
function getCurrentUserRole() {
|
|
|
|
|
try {
|
|
|
|
|
const userStr = localStorage.getItem('atlas_user');
|
|
|
|
|
if (userStr) {
|
|
|
|
|
const user = JSON.parse(userStr);
|
|
|
|
|
return (user.role || '').toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Error parsing user data:', e);
|
|
|
|
|
}
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if current user is viewer
|
|
|
|
|
function isViewer() {
|
|
|
|
|
return getCurrentUserRole() === 'viewer';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hide create/update/delete buttons for viewer role
|
|
|
|
|
function hideButtonsForViewer() {
|
|
|
|
|
if (isViewer()) {
|
|
|
|
|
// Hide create/control buttons
|
|
|
|
|
document.querySelectorAll('button').forEach(btn => {
|
|
|
|
|
const text = btn.textContent || '';
|
|
|
|
|
const onclick = btn.getAttribute('onclick') || '';
|
|
|
|
|
if (text.includes('Create') || text.includes('Service Control') || text.includes('Add') ||
|
|
|
|
|
(text.includes('Edit') && !text.includes('Refresh')) || text.includes('Delete') ||
|
|
|
|
|
onclick.includes('showCreate') || onclick.includes('showServiceControl') ||
|
|
|
|
|
onclick.includes('showCreateChanger') || onclick.includes('showEditChanger') ||
|
|
|
|
|
onclick.includes('deleteChanger') || onclick.includes('showCreateDriveModal') ||
|
|
|
|
|
onclick.includes('showEditDriveModal') || onclick.includes('deleteDrive')) {
|
|
|
|
|
btn.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getAuthHeaders() {
|
|
|
|
|
const token = localStorage.getItem('atlas_token');
|
|
|
|
|
return {
|
|
|
|
|
@@ -342,7 +492,11 @@
|
|
|
|
|
// Load Drives
|
|
|
|
|
async function loadVTLDrives() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/v1/vtl/drives', { headers: getAuthHeaders() });
|
|
|
|
|
// Add cache busting to ensure fresh data
|
|
|
|
|
const res = await fetch('/api/v1/vtl/drives?_=' + Date.now(), {
|
|
|
|
|
headers: getAuthHeaders(),
|
|
|
|
|
cache: 'no-cache'
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) throw new Error('Failed to load drives');
|
|
|
|
|
|
|
|
|
|
const drives = await res.json();
|
|
|
|
|
@@ -380,6 +534,8 @@
|
|
|
|
|
${drive.media_loaded
|
|
|
|
|
? `<button onclick="ejectTape(${drive.id})" class="px-3 py-1.5 bg-yellow-600 hover:bg-yellow-700 text-white rounded text-sm">Eject</button>`
|
|
|
|
|
: `<button onclick="loadTape(${drive.id})" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">Load Tape</button>`}
|
|
|
|
|
<button onclick="showEditDriveModal(${drive.id})" class="px-3 py-1.5 bg-slate-600 hover:bg-slate-700 text-white rounded text-sm">Edit</button>
|
|
|
|
|
<button onclick="deleteDrive(${drive.id})" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">Delete</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -449,7 +605,12 @@
|
|
|
|
|
// Load Media Changer
|
|
|
|
|
async function loadMediaChanger() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/v1/vtl/changer/status', { headers: getAuthHeaders() });
|
|
|
|
|
// Use /api/v1/vtl/changers instead of /api/v1/vtl/changer/status for better consistency
|
|
|
|
|
// Add cache busting to force refresh
|
|
|
|
|
const res = await fetch('/api/v1/vtl/changers?t=' + Date.now(), {
|
|
|
|
|
headers: getAuthHeaders(),
|
|
|
|
|
cache: 'no-cache'
|
|
|
|
|
});
|
|
|
|
|
const changerEl = document.getElementById('vtl-changer-content');
|
|
|
|
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
@@ -478,17 +639,29 @@
|
|
|
|
|
|
|
|
|
|
changerEl.innerHTML = changerList.map(changer => `
|
|
|
|
|
<div class="bg-slate-900 rounded-lg p-6 border border-slate-700 mb-4">
|
|
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
|
|
<h3 class="text-lg font-semibold text-white">Media Changer ${changer.id || 'N/A'}</h3>
|
|
|
|
|
${changer.status === 'online'
|
|
|
|
|
? '<span class="px-2 py-1 rounded text-xs font-medium bg-green-900 text-green-300">Online</span>'
|
|
|
|
|
: '<span class="px-2 py-1 rounded text-xs font-medium bg-red-900 text-red-300">Offline</span>'}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-sm text-slate-400 space-y-2">
|
|
|
|
|
<p><span class="text-slate-300">Device:</span> ${changer.device || 'N/A'}</p>
|
|
|
|
|
<p><span class="text-slate-300">Library ID:</span> ${changer.library_id || 'N/A'}</p>
|
|
|
|
|
<p><span class="text-slate-300">Slots:</span> ${changer.slots || 0}</p>
|
|
|
|
|
<p><span class="text-slate-300">Drives:</span> ${changer.drives || 0}</p>
|
|
|
|
|
<div class="flex items-start justify-between">
|
|
|
|
|
<div class="flex-1">
|
|
|
|
|
<div class="flex items-center gap-2 mb-4">
|
|
|
|
|
<h3 class="text-lg font-semibold text-white">Media Changer ${changer.id || changer.library_id || 'N/A'}</h3>
|
|
|
|
|
${changer.status === 'online'
|
|
|
|
|
? '<span class="px-2 py-1 rounded text-xs font-medium bg-green-900 text-green-300">Online</span>'
|
|
|
|
|
: '<span class="px-2 py-1 rounded text-xs font-medium bg-red-900 text-red-300">Offline</span>'}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-sm text-slate-400 space-y-2">
|
|
|
|
|
<p><span class="text-slate-300">Device:</span> <span class="text-slate-400 font-mono">${changer.device || 'N/A'}</span></p>
|
|
|
|
|
<p><span class="text-slate-300">Library ID:</span> <span class="text-slate-400">${changer.library_id || changer.id || 'N/A'}</span></p>
|
|
|
|
|
<p><span class="text-slate-300">Slots:</span> <span class="text-slate-400">${changer.slots || 0}</span></p>
|
|
|
|
|
<p><span class="text-slate-300">Drives:</span> <span class="text-slate-400">${changer.drives || 0}</span></p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex gap-2 ml-4">
|
|
|
|
|
<button onclick="showEditChangerModal(${changer.library_id || changer.id})" class="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
|
|
|
|
|
Edit
|
|
|
|
|
</button>
|
|
|
|
|
<button onclick="deleteChanger(${changer.library_id || changer.id})" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
@@ -833,6 +1006,324 @@
|
|
|
|
|
document.getElementById(modalId).classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Media Changer Management Functions
|
|
|
|
|
function showCreateChangerModal() {
|
|
|
|
|
document.getElementById('changer-modal-title').textContent = 'Add Media Changer';
|
|
|
|
|
document.getElementById('changer-form').reset();
|
|
|
|
|
document.getElementById('changer-library-id').value = '';
|
|
|
|
|
document.getElementById('changer-id-input').removeAttribute('readonly');
|
|
|
|
|
document.getElementById('changer-id-input').disabled = false;
|
|
|
|
|
document.getElementById('changer-slots-div').style.display = 'block';
|
|
|
|
|
document.getElementById('changer-modal').classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showEditChangerModal(libraryID) {
|
|
|
|
|
document.getElementById('changer-modal-title').textContent = 'Edit Media Changer';
|
|
|
|
|
document.getElementById('changer-form').reset();
|
|
|
|
|
document.getElementById('changer-library-id').value = libraryID;
|
|
|
|
|
document.getElementById('changer-id-input').value = libraryID;
|
|
|
|
|
document.getElementById('changer-id-input').setAttribute('readonly', 'readonly');
|
|
|
|
|
document.getElementById('changer-id-input').disabled = true;
|
|
|
|
|
document.getElementById('changer-slots-div').style.display = 'none';
|
|
|
|
|
|
|
|
|
|
// Load current changer data
|
|
|
|
|
fetch(`/api/v1/vtl/changers/${libraryID}`, { headers: getAuthHeaders() })
|
|
|
|
|
.then(res => res.json())
|
|
|
|
|
.then(changer => {
|
|
|
|
|
if (changer.library_id || changer.id) {
|
|
|
|
|
document.getElementById('changer-vendor-input').value = changer.vendor || 'STK';
|
|
|
|
|
document.getElementById('changer-product-input').value = changer.product || '';
|
|
|
|
|
document.getElementById('changer-serial-input').value = changer.serial || '';
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch(err => {
|
|
|
|
|
console.error('Error loading changer data:', err);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('changer-modal').classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveChanger(e) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const formData = new FormData(e.target);
|
|
|
|
|
// Get library_id directly from the visible input field (not from FormData to avoid conflict with hidden input)
|
|
|
|
|
const libraryIDInput = document.getElementById('changer-id-input');
|
|
|
|
|
const libraryID = libraryIDInput ? parseInt(libraryIDInput.value) : parseInt(formData.get('library_id'));
|
|
|
|
|
const isEdit = document.getElementById('changer-library-id').value !== '';
|
|
|
|
|
|
|
|
|
|
// Validate library ID
|
|
|
|
|
if (!libraryID || libraryID <= 0 || isNaN(libraryID)) {
|
|
|
|
|
alert('Error: Library ID must be a valid number greater than 0');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
library_id: libraryID,
|
|
|
|
|
vendor: formData.get('vendor') || '',
|
|
|
|
|
product: formData.get('product') || '',
|
|
|
|
|
serial: formData.get('serial') || '',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!isEdit) {
|
|
|
|
|
const numSlotsInput = document.getElementById('changer-slots-input');
|
|
|
|
|
data.num_slots = numSlotsInput ? parseInt(numSlotsInput.value) : parseInt(formData.get('num_slots')) || 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let res;
|
|
|
|
|
if (isEdit) {
|
|
|
|
|
res = await fetch(`/api/v1/vtl/changers/${libraryID}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: getAuthHeaders(),
|
|
|
|
|
body: JSON.stringify(data)
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
res = await fetch('/api/v1/vtl/changers', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: getAuthHeaders(),
|
|
|
|
|
body: JSON.stringify(data)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await res.json().catch(() => null);
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
closeModal('changer-modal');
|
|
|
|
|
// Wait a moment for backend to finish writing file
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
|
// Force refresh with cache busting
|
|
|
|
|
await loadMediaChanger();
|
|
|
|
|
await refreshVTLStatus();
|
|
|
|
|
alert(isEdit ? 'Media changer updated successfully' : 'Media changer created successfully');
|
|
|
|
|
} else {
|
|
|
|
|
const errorMsg = (result && result.message) ? result.message : 'Failed to save media changer';
|
|
|
|
|
alert(`Error: ${errorMsg}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert(`Error: ${err.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteChanger(libraryID) {
|
|
|
|
|
if (!confirm(`Are you sure you want to delete media changer Library ${libraryID}? This will also remove all associated drives.`)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/v1/vtl/changers/${libraryID}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: getAuthHeaders()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await res.json().catch(() => null);
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
alert('Media changer deleted successfully');
|
|
|
|
|
loadMediaChanger();
|
|
|
|
|
refreshVTLStatus();
|
|
|
|
|
} else {
|
|
|
|
|
const errorMsg = (result && result.message) ? result.message : 'Failed to delete media changer';
|
|
|
|
|
alert(`Error: ${errorMsg}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert(`Error: ${err.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drive product options by vendor
|
|
|
|
|
const driveProducts = {
|
|
|
|
|
'IBM': [
|
|
|
|
|
{ value: 'ULT3580-TD5', label: 'ULT3580-TD5 (LTO-5)' },
|
|
|
|
|
{ value: 'ULT3580-TD6', label: 'ULT3580-TD6 (LTO-6)' },
|
|
|
|
|
{ value: 'ULT3580-TD7', label: 'ULT3580-TD7 (LTO-7)' },
|
|
|
|
|
{ value: 'ULT3580-TD8', label: 'ULT3580-TD8 (LTO-8)' }
|
|
|
|
|
],
|
|
|
|
|
'HP': [
|
|
|
|
|
{ value: 'HP LTO-5', label: 'HP LTO-5' },
|
|
|
|
|
{ value: 'HP LTO-6', label: 'HP LTO-6' },
|
|
|
|
|
{ value: 'HP LTO-7', label: 'HP LTO-7' },
|
|
|
|
|
{ value: 'HP LTO-8', label: 'HP LTO-8' }
|
|
|
|
|
],
|
|
|
|
|
'Quantum': [
|
|
|
|
|
{ value: 'Quantum LTO-5', label: 'Quantum LTO-5' },
|
|
|
|
|
{ value: 'Quantum LTO-6', label: 'Quantum LTO-6' },
|
|
|
|
|
{ value: 'Quantum LTO-7', label: 'Quantum LTO-7' },
|
|
|
|
|
{ value: 'Quantum LTO-8', label: 'Quantum LTO-8' }
|
|
|
|
|
],
|
|
|
|
|
'Tandberg': [
|
|
|
|
|
{ value: 'Tandberg LTO-5', label: 'Tandberg LTO-5' },
|
|
|
|
|
{ value: 'Tandberg LTO-6', label: 'Tandberg LTO-6' },
|
|
|
|
|
{ value: 'Tandberg LTO-7', label: 'Tandberg LTO-7' },
|
|
|
|
|
{ value: 'Tandberg LTO-8', label: 'Tandberg LTO-8' }
|
|
|
|
|
],
|
|
|
|
|
'Overland': [
|
|
|
|
|
{ value: 'Overland LTO-5', label: 'Overland LTO-5' },
|
|
|
|
|
{ value: 'Overland LTO-6', label: 'Overland LTO-6' },
|
|
|
|
|
{ value: 'Overland LTO-7', label: 'Overland LTO-7' },
|
|
|
|
|
{ value: 'Overland LTO-8', label: 'Overland LTO-8' }
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function updateDriveProductOptions() {
|
|
|
|
|
const vendorSelect = document.getElementById('drive-vendor-input');
|
|
|
|
|
const productSelect = document.getElementById('drive-product-input');
|
|
|
|
|
const vendor = vendorSelect.value;
|
|
|
|
|
|
|
|
|
|
// Clear existing options
|
|
|
|
|
productSelect.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
// Add options for selected vendor
|
|
|
|
|
if (driveProducts[vendor]) {
|
|
|
|
|
driveProducts[vendor].forEach(product => {
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
option.value = product.value;
|
|
|
|
|
option.textContent = product.label;
|
|
|
|
|
productSelect.appendChild(option);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drive Management Functions
|
|
|
|
|
function showCreateDriveModal() {
|
|
|
|
|
document.getElementById('drive-modal-title').textContent = 'Add Drive';
|
|
|
|
|
document.getElementById('drive-form').reset();
|
|
|
|
|
document.getElementById('drive-id-input').removeAttribute('readonly');
|
|
|
|
|
document.getElementById('drive-id-input').disabled = false;
|
|
|
|
|
// Set default vendor to IBM
|
|
|
|
|
document.getElementById('drive-vendor-input').value = 'IBM';
|
|
|
|
|
updateDriveProductOptions();
|
|
|
|
|
document.getElementById('drive-modal').classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function showEditDriveModal(driveID) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/v1/vtl/drives/${driveID}`, { headers: getAuthHeaders() });
|
|
|
|
|
if (!res.ok) throw new Error('Failed to load drive');
|
|
|
|
|
|
|
|
|
|
const drive = await res.json();
|
|
|
|
|
|
|
|
|
|
document.getElementById('drive-modal-title').textContent = 'Edit Drive';
|
|
|
|
|
document.getElementById('drive-id-input').value = drive.id;
|
|
|
|
|
document.getElementById('drive-id-input').setAttribute('readonly', 'readonly');
|
|
|
|
|
document.getElementById('drive-id-input').disabled = true;
|
|
|
|
|
document.getElementById('drive-library-id-input').value = drive.library_id || '';
|
|
|
|
|
document.getElementById('drive-slot-id-input').value = drive.slot_id || '';
|
|
|
|
|
|
|
|
|
|
// Set vendor and update product options
|
|
|
|
|
const vendor = drive.vendor || 'IBM';
|
|
|
|
|
document.getElementById('drive-vendor-input').value = vendor;
|
|
|
|
|
updateDriveProductOptions();
|
|
|
|
|
|
|
|
|
|
// Set product (try to match existing value or default to first option)
|
|
|
|
|
const productSelect = document.getElementById('drive-product-input');
|
|
|
|
|
const product = drive.product || '';
|
|
|
|
|
if (product) {
|
|
|
|
|
// Try to find matching option
|
|
|
|
|
let found = false;
|
|
|
|
|
for (let i = 0; i < productSelect.options.length; i++) {
|
|
|
|
|
if (productSelect.options[i].value === product) {
|
|
|
|
|
productSelect.value = product;
|
|
|
|
|
found = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If not found, add it as a custom option
|
|
|
|
|
if (!found) {
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
option.value = product;
|
|
|
|
|
option.textContent = product + ' (Custom)';
|
|
|
|
|
option.selected = true;
|
|
|
|
|
productSelect.appendChild(option);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.getElementById('drive-serial-input').value = drive.serial || '';
|
|
|
|
|
|
|
|
|
|
document.getElementById('drive-modal').classList.remove('hidden');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert(`Error: ${err.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveDrive(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
const formData = new FormData(event.target);
|
|
|
|
|
const driveID = parseInt(document.getElementById('drive-id-input').value);
|
|
|
|
|
const isEdit = document.getElementById('drive-id-input').hasAttribute('readonly');
|
|
|
|
|
|
|
|
|
|
const data = {
|
|
|
|
|
drive_id: driveID,
|
|
|
|
|
library_id: parseInt(document.getElementById('drive-library-id-input').value),
|
|
|
|
|
slot_id: parseInt(document.getElementById('drive-slot-id-input').value),
|
|
|
|
|
vendor: document.getElementById('drive-vendor-input').value || '',
|
|
|
|
|
product: document.getElementById('drive-product-input').value || '',
|
|
|
|
|
serial: document.getElementById('drive-serial-input').value || ''
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let res;
|
|
|
|
|
if (isEdit) {
|
|
|
|
|
res = await fetch(`/api/v1/vtl/drives/${driveID}`, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: getAuthHeaders(),
|
|
|
|
|
body: JSON.stringify(data)
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
res = await fetch('/api/v1/vtl/drives', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: getAuthHeaders(),
|
|
|
|
|
body: JSON.stringify(data)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await res.json().catch(() => null);
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
closeModal('drive-modal');
|
|
|
|
|
// Wait a moment for backend to finish writing file
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
|
// Force refresh with cache busting
|
|
|
|
|
await loadVTLDrives();
|
|
|
|
|
await refreshVTLStatus();
|
|
|
|
|
alert(isEdit ? 'Drive updated successfully' : 'Drive created successfully');
|
|
|
|
|
} else {
|
|
|
|
|
const errorMsg = (result && result.message) ? result.message : 'Failed to save drive';
|
|
|
|
|
alert(`Error: ${errorMsg}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert(`Error: ${err.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteDrive(driveID) {
|
|
|
|
|
if (!confirm(`Are you sure you want to delete Drive ${driveID}?`)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/v1/vtl/drives/${driveID}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: getAuthHeaders()
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await res.json().catch(() => null);
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
alert('Drive deleted successfully');
|
|
|
|
|
// Wait a moment for backend to finish writing file
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
|
loadVTLDrives();
|
|
|
|
|
refreshVTLStatus();
|
|
|
|
|
} else {
|
|
|
|
|
const errorMsg = (result && result.message) ? result.message : 'Failed to delete drive';
|
|
|
|
|
alert(`Error: ${errorMsg}`);
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert(`Error: ${err.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update Media Changer Status in dashboard
|
|
|
|
|
async function updateMediaChangerStatus() {
|
|
|
|
|
try {
|
|
|
|
|
|