Files
atlas/web/templates/vtl.html
2025-12-23 07:50:08 +00:00

1382 lines
59 KiB
HTML

{{define "vtl-content"}}
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-white mb-2">Virtual Tape Library</h1>
<p class="text-slate-400">Manage mhvtl drives, tapes, and media changer</p>
</div>
<div class="flex gap-2">
<button onclick="refreshVTLStatus()" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg text-sm font-medium">
Refresh
</button>
<button onclick="showServiceControl()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium">
Service Control
</button>
</div>
</div>
<!-- VTL Status Overview -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white">Service</h2>
<div class="h-10 w-10 rounded-lg bg-slate-700 flex items-center justify-center">
<svg class="w-6 h-6 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
</div>
<p class="text-2xl font-bold text-white mb-1" id="vtl-service-status">-</p>
<p class="text-sm text-slate-400">mhvtl Service</p>
</div>
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white">Drives</h2>
</div>
<p class="text-2xl font-bold text-white mb-1" id="vtl-drives-online">-</p>
<p class="text-sm text-slate-400"><span id="vtl-drives-total">-</span> Total</p>
</div>
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white">Tapes</h2>
</div>
<p class="text-2xl font-bold text-white mb-1" id="vtl-tapes-available">-</p>
<p class="text-sm text-slate-400"><span id="vtl-tapes-total">-</span> Total</p>
</div>
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-white">Media Changer</h2>
</div>
<p class="text-2xl font-bold text-white mb-1" id="vtl-changer-status">-</p>
<p class="text-sm text-slate-400" id="vtl-changer-detail">Libraries</p>
</div>
</div>
<!-- Tabs for Drives, Tapes, and Media Changer -->
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
<div class="flex border-b border-slate-700">
<button onclick="switchVTLTab('drives')" id="vtl-tab-drives" class="flex-1 px-4 py-3 text-sm font-medium text-center border-b-2 border-blue-600 text-blue-400 bg-slate-800">
Tape Drives
</button>
<button onclick="switchVTLTab('tapes')" id="vtl-tab-tapes" class="flex-1 px-4 py-3 text-sm font-medium text-center border-b-2 border-transparent text-slate-400 hover:text-white">
Tape Media
</button>
<button onclick="switchVTLTab('changer')" id="vtl-tab-changer" class="flex-1 px-4 py-3 text-sm font-medium text-center border-b-2 border-transparent text-slate-400 hover:text-white">
Media Changer
</button>
</div>
<!-- Drives Tab -->
<div id="vtl-content-drives" class="tab-content">
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
<div>
<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>
<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>
</div>
</div>
<!-- Tapes Tab -->
<div id="vtl-content-tapes" class="tab-content hidden">
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
<div>
<h2 class="text-lg font-semibold text-white">Virtual Tape Media</h2>
<p class="text-xs text-slate-400 mt-1">Create, manage, and delete virtual tapes</p>
</div>
<div class="flex gap-2">
<button onclick="loadVTLTapes()" class="text-sm text-slate-400 hover:text-white">Refresh</button>
<button onclick="showCreateTapeModal()" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg text-sm font-medium">
Create Tape
</button>
<button onclick="showBulkCreateTapeModal()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium">
Bulk Create
</button>
</div>
</div>
<div id="vtl-tapes-list" class="p-4">
<p class="text-slate-400 text-sm">Loading...</p>
</div>
</div>
<!-- Media Changer Tab -->
<div id="vtl-content-changer" class="tab-content hidden">
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
<div>
<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>
<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>
</div>
</div>
</div>
</div>
<!-- Create Tape Modal -->
<div id="create-tape-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 max-h-[90vh] overflow-y-auto">
<h3 class="text-xl font-semibold text-white mb-4">Create Virtual Tape</h3>
<form id="create-tape-form" onsubmit="createTape(event)" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Barcode <span class="text-red-400">*</span></label>
<input type="text" name="barcode" id="tape-barcode" placeholder="E01001L8" 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">Must end with LTO suffix (L1-L9, LW, LT, LU, LV). Example: E01001L8</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Library ID <span class="text-red-400">*</span></label>
<select name="library_id" id="tape-library-id" 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="">Select Library...</option>
</select>
<p class="text-xs text-slate-400 mt-1">Library where tape will be placed</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Slot ID <span class="text-red-400">*</span></label>
<input type="number" name="slot_id" id="tape-slot-id" placeholder="1" required min="1" 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 (must be greater than 0)</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Tape Type</label>
<select name="type" id="tape-type" 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="">Auto-detect from barcode</option>
<option value="LTO-5">LTO-5 (3 TB compressed)</option>
<option value="LTO-6">LTO-6 (6.25 TB compressed)</option>
<option value="LTO-7">LTO-7 (15 TB compressed)</option>
<option value="LTO-8">LTO-8 (30 TB compressed)</option>
<option value="LTO-9">LTO-9 (45 TB compressed)</option>
</select>
<p class="text-xs text-slate-400 mt-1">Leave empty to auto-detect from barcode suffix</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Size (bytes)</label>
<input type="number" name="size" id="tape-size" value="0" min="0" class="w-full px-3 py-2 bg-slate-900 border border-slate-700 rounded text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-600">
<p class="text-xs text-slate-400 mt-1">Leave 0 to use default size for tape generation</p>
</div>
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('create-tape-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">
Create
</button>
</div>
</form>
</div>
</div>
<!-- Bulk Create Tape Modal -->
<div id="bulk-create-tape-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 max-h-[90vh] overflow-y-auto">
<h3 class="text-xl font-semibold text-white mb-4">Bulk Create Virtual Tapes</h3>
<form id="bulk-create-tape-form" onsubmit="bulkCreateTapes(event)" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Prefix <span class="text-red-400">*</span></label>
<input type="text" name="prefix" id="bulk-tape-prefix" placeholder="AVT" 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">Barcode prefix (e.g., "AVT" will create AVT001L8, AVT002L8, ...). Suffix LTO will be added automatically.</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Library ID <span class="text-red-400">*</span></label>
<select name="library_id" id="bulk-tape-library-id" 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="">Select Library...</option>
</select>
<p class="text-xs text-slate-400 mt-1">Library where tapes will be placed</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Start Slot ID <span class="text-red-400">*</span></label>
<input type="number" name="start_slot" id="bulk-tape-start-slot" placeholder="1" required min="1" 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">Starting slot number (tapes will be placed in consecutive slots)</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Count</label>
<input type="number" name="count" id="bulk-tape-count" value="10" min="1" max="100" 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">Number of tapes to create</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Start Number</label>
<input type="number" name="start" id="bulk-tape-start" value="1" 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">Starting number for barcode sequence</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-300 mb-1">Tape Type</label>
<select name="type" id="bulk-tape-type" 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="">Auto-detect from suffix</option>
<option value="LTO-5">LTO-5 (3 TB compressed)</option>
<option value="LTO-6">LTO-6 (6.25 TB compressed)</option>
<option value="LTO-7">LTO-7 (15 TB compressed)</option>
<option value="LTO-8">LTO-8 (30 TB compressed)</option>
<option value="LTO-9">LTO-9 (45 TB compressed)</option>
</select>
<p class="text-xs text-slate-400 mt-1">Leave empty to auto-detect from barcode suffix (L1-L9, LW, LT, LU, LV)</p>
</div>
<div class="flex gap-2 justify-end">
<button type="button" onclick="closeModal('bulk-create-tape-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 All
</button>
</div>
</form>
</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">
<h3 class="text-xl font-semibold text-white mb-4">mhvtl Service Control</h3>
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-slate-300">Service Status</span>
<span id="service-status-badge" class="px-3 py-1 rounded-full text-xs font-medium bg-slate-700 text-slate-300">-</span>
</div>
<div class="flex gap-2 justify-end">
<button onclick="controlVTLService('start')" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded text-sm">
Start
</button>
<button onclick="controlVTLService('stop')" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
Stop
</button>
<button onclick="controlVTLService('restart')" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm">
Restart
</button>
<button onclick="closeModal('service-control-modal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded text-sm">
Close
</button>
</div>
</div>
</div>
</div>
<script>
// Check authentication on page load
(function() {
const token = localStorage.getItem('atlas_token');
if (!token) {
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 {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {})
};
}
function formatBytes(bytes) {
if (!bytes || bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
// Tab switching
let currentVTLTab = 'drives';
function switchVTLTab(tab) {
currentVTLTab = tab;
// Update tab buttons
['drives', 'tapes', 'changer'].forEach(t => {
const tabEl = document.getElementById(`vtl-tab-${t}`);
const contentEl = document.getElementById(`vtl-content-${t}`);
if (t === tab) {
tabEl.classList.add('border-blue-600', 'text-blue-400');
tabEl.classList.remove('border-transparent', 'text-slate-400');
contentEl.classList.remove('hidden');
} else {
tabEl.classList.remove('border-blue-600', 'text-blue-400');
tabEl.classList.add('border-transparent', 'text-slate-400');
contentEl.classList.add('hidden');
}
});
// Load data for active tab
if (tab === 'drives') loadVTLDrives();
else if (tab === 'tapes') loadVTLTapes();
else if (tab === 'changer') loadMediaChanger();
}
// VTL Status
async function refreshVTLStatus() {
try {
const res = await fetch('/api/v1/vtl/status', { headers: getAuthHeaders() });
if (!res.ok) throw new Error('Failed to fetch VTL status');
const status = await res.json();
// Update status cards
document.getElementById('vtl-service-status').textContent = status.service_running ? 'Running' : 'Stopped';
document.getElementById('vtl-service-status').className = status.service_running
? 'text-2xl font-bold text-green-400 mb-1'
: 'text-2xl font-bold text-red-400 mb-1';
document.getElementById('vtl-drives-online').textContent = status.drives_online || 0;
document.getElementById('vtl-drives-total').textContent = status.drives_total || 0;
document.getElementById('vtl-tapes-available').textContent = status.tapes_available || 0;
document.getElementById('vtl-tapes-total').textContent = status.tapes_total || 0;
// Update media changer status
updateMediaChangerStatus();
// Refresh current tab
if (currentVTLTab === 'drives') loadVTLDrives();
else if (currentVTLTab === 'tapes') loadVTLTapes();
else if (currentVTLTab === 'changer') loadMediaChanger();
} catch (err) {
console.error('Error refreshing VTL status:', err);
}
}
// Load Drives
async function loadVTLDrives() {
try {
// 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();
const listEl = document.getElementById('vtl-drives-list');
if (!Array.isArray(drives) || drives.length === 0) {
listEl.innerHTML = '<p class="text-slate-400 text-sm">No tape drives found. Make sure mhvtl is installed and configured.</p>';
return;
}
listEl.innerHTML = drives.map(drive => `
<div class="border-b border-slate-700 last:border-0 py-4">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-lg font-semibold text-white">Drive ${drive.id}</h3>
${drive.library_id ? `<span class="px-2 py-1 rounded text-xs font-medium bg-blue-900 text-blue-300">Library ${drive.library_id}</span>` : ''}
${drive.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>'}
<span class="px-2 py-1 rounded text-xs font-medium bg-slate-700 text-slate-300">${drive.type || 'Unknown'}</span>
</div>
<div class="text-sm text-slate-400 space-y-1">
<p><span class="text-slate-300">Library ID:</span> ${drive.library_id || 'N/A'}</p>
<p><span class="text-slate-300">Device:</span> ${drive.device}</p>
<p><span class="text-slate-300">Vendor:</span> ${drive.vendor || 'Unknown'}</p>
<p><span class="text-slate-300">Product:</span> ${drive.product || 'Unknown'}</p>
${drive.slot_id > 0 ? `<p><span class="text-slate-300">Slot:</span> ${drive.slot_id}</p>` : ''}
${drive.media_loaded
? `<p><span class="text-slate-300">Media:</span> <span class="text-green-300">Loaded - ${drive.barcode || 'Unknown'}</span></p>`
: '<p><span class="text-slate-300">Media:</span> <span class="text-slate-500">Not Loaded</span></p>'}
</div>
</div>
<div class="flex gap-2 ml-4">
${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>
`).join('');
} catch (err) {
document.getElementById('vtl-drives-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
}
}
// Load Tapes
async function loadVTLTapes() {
try {
// Add timestamp to prevent caching
const res = await fetch('/api/v1/vtl/tapes?_=' + Date.now(), { headers: getAuthHeaders() });
if (!res.ok) throw new Error('Failed to load tapes');
const tapes = await res.json();
const listEl = document.getElementById('vtl-tapes-list');
console.log('Loaded tapes:', tapes); // Debug log
if (!Array.isArray(tapes) || tapes.length === 0) {
listEl.innerHTML = '<p class="text-slate-400 text-sm">No tapes found. Create your first virtual tape.</p>';
return;
}
listEl.innerHTML = `
<div class="mb-4 flex items-center justify-between">
<span class="text-sm text-slate-400">Total: ${tapes.length} tapes</span>
<button onclick="bulkDeleteTapes()" class="px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
Bulk Delete
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
${tapes.map(tape => `
<div class="bg-slate-900 rounded-lg p-4 border border-slate-700">
<div class="flex items-center justify-between mb-2">
<h3 class="text-lg font-semibold text-white">${tape.barcode}</h3>
${tape.status === 'available'
? '<span class="px-2 py-1 rounded text-xs font-medium bg-green-900 text-green-300">Available</span>'
: tape.status === 'in_use'
? '<span class="px-2 py-1 rounded text-xs font-medium bg-yellow-900 text-yellow-300">In Use</span>'
: '<span class="px-2 py-1 rounded text-xs font-medium bg-red-900 text-red-300">Error</span>'}
</div>
<div class="text-sm text-slate-400 space-y-1 mb-3">
<p><span class="text-slate-300">Type:</span> ${tape.type || 'Unknown'}</p>
${tape.library_id ? `<p><span class="text-slate-300">Library ID:</span> ${tape.library_id}</p>` : ''}
${tape.slot_id > 0 ? `<p><span class="text-slate-300">Slot:</span> ${tape.slot_id}</p>` : ''}
<p><span class="text-slate-300">Size:</span> ${formatBytes(tape.size || 0)}</p>
<p><span class="text-slate-300">Used:</span> ${formatBytes(tape.used || 0)}</p>
${tape.drive_id >= 0 ? `<p><span class="text-slate-300">Loaded in:</span> Drive ${tape.drive_id}</p>` : ''}
</div>
<div class="flex gap-2">
<button onclick="deleteTape('${tape.barcode}')" class="flex-1 px-3 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded text-sm">
Delete
</button>
</div>
</div>
`).join('')}
</div>
`;
} catch (err) {
document.getElementById('vtl-tapes-list').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
}
}
// Load Media Changer
async function loadMediaChanger() {
try {
// 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) {
changerEl.innerHTML = `
<div class="bg-slate-900 rounded-lg p-6 border border-slate-700">
<p class="text-slate-400 text-sm mb-4">No media changer found or mhvtl not configured.</p>
<p class="text-slate-500 text-xs">This feature requires mhvtl media changer configuration.</p>
</div>
`;
return;
}
const changers = await res.json();
// Handle both single changer and array of changers
const changerList = Array.isArray(changers) ? changers : [changers];
if (changerList.length === 0) {
changerEl.innerHTML = `
<div class="bg-slate-900 rounded-lg p-6 border border-slate-700">
<p class="text-slate-400 text-sm mb-4">No media changer found.</p>
</div>
`;
return;
}
changerEl.innerHTML = changerList.map(changer => `
<div class="bg-slate-900 rounded-lg p-6 border border-slate-700 mb-4">
<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('');
} catch (err) {
document.getElementById('vtl-changer-content').innerHTML = `<p class="text-red-400 text-sm">Error: ${err.message}</p>`;
}
}
// Create Tape
async function showCreateTapeModal() {
// Load available libraries
try {
const res = await fetch('/api/v1/vtl/changers', { headers: getAuthHeaders() });
if (res.ok) {
const changers = await res.json();
const changerList = Array.isArray(changers) ? changers : [changers];
const librarySelect = document.getElementById('tape-library-id');
librarySelect.innerHTML = '<option value="">Select Library...</option>';
changerList.forEach(changer => {
const option = document.createElement('option');
option.value = changer.library_id || changer.id;
option.textContent = `Library ${changer.library_id || changer.id} (${changer.slots || 0} slots)`;
librarySelect.appendChild(option);
});
}
} catch (err) {
console.error('Failed to load libraries:', err);
}
document.getElementById('create-tape-modal').classList.remove('hidden');
}
async function createTape(e) {
e.preventDefault();
const formData = new FormData(e.target);
const libraryID = parseInt(formData.get('library_id'));
const slotID = parseInt(formData.get('slot_id'));
if (!libraryID || libraryID <= 0) {
alert('Error: Please select a valid Library ID');
return;
}
if (!slotID || slotID <= 0) {
alert('Error: Please enter a valid Slot ID (must be greater than 0)');
return;
}
try {
const res = await fetch('/api/v1/vtl/tapes', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
barcode: formData.get('barcode'),
type: formData.get('type') || '',
size: parseInt(formData.get('size')) || 0,
library_id: libraryID,
slot_id: slotID
})
});
if (res.ok) {
closeModal('create-tape-modal');
e.target.reset();
// Wait a bit for mhvtl service to restart and refresh
alert('Tape created successfully. Refreshing...');
setTimeout(() => {
loadVTLTapes();
refreshVTLStatus();
}, 2000); // Wait 2 seconds for service restart
} else {
const err = await res.json().catch(() => ({ error: 'Failed to create tape' }));
alert(`Error: ${err.error || err.message || 'Failed to create tape'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
// Bulk Create Tapes
async function showBulkCreateTapeModal() {
// Load available libraries
try {
const res = await fetch('/api/v1/vtl/changers', { headers: getAuthHeaders() });
if (res.ok) {
const changers = await res.json();
const changerList = Array.isArray(changers) ? changers : [changers];
const librarySelect = document.getElementById('bulk-tape-library-id');
librarySelect.innerHTML = '<option value="">Select Library...</option>';
changerList.forEach(changer => {
const option = document.createElement('option');
option.value = changer.library_id || changer.id;
option.textContent = `Library ${changer.library_id || changer.id} (${changer.slots || 0} slots)`;
librarySelect.appendChild(option);
});
}
} catch (err) {
console.error('Failed to load libraries:', err);
}
document.getElementById('bulk-create-tape-modal').classList.remove('hidden');
}
async function bulkCreateTapes(e) {
e.preventDefault();
const formData = new FormData(e.target);
const prefix = formData.get('prefix');
const libraryID = parseInt(formData.get('library_id'));
const startSlot = parseInt(formData.get('start_slot'));
const count = parseInt(formData.get('count'));
const start = parseInt(formData.get('start'));
const type = formData.get('type') || '';
if (!libraryID || libraryID <= 0) {
alert('Error: Please select a valid Library ID');
return;
}
if (!startSlot || startSlot <= 0) {
alert('Error: Please enter a valid Start Slot ID (must be greater than 0)');
return;
}
// Determine LTO suffix from type, or use default L8
let ltoSuffix = 'L8'; // Default to LTO-8
if (type) {
const typeMap = {
'LTO-1': 'L1', 'LTO-2': 'L2', 'LTO-3': 'L3', 'LTO-4': 'L4',
'LTO-5': 'L5', 'LTO-6': 'L6', 'LTO-7': 'L7', 'LTO-8': 'L8', 'LTO-9': 'L9'
};
ltoSuffix = typeMap[type] || 'L8';
}
if (!confirm(`Create ${count} tapes with prefix "${prefix}" starting from ${start} in Library ${libraryID}, slots ${startSlot} to ${startSlot + count - 1}?`)) {
return;
}
let success = 0;
let failed = 0;
for (let i = 0; i < count; i++) {
const barcode = `${prefix}${String(start + i).padStart(3, '0')}${ltoSuffix}`;
const slotID = startSlot + i;
try {
const res = await fetch('/api/v1/vtl/tapes', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
barcode: barcode,
type: type,
size: 0,
library_id: libraryID,
slot_id: slotID
})
});
if (res.ok) {
success++;
} else {
const err = await res.json().catch(() => ({ error: 'Failed to create tape' }));
console.error(`Failed to create tape ${barcode}:`, err);
failed++;
}
} catch (err) {
console.error(`Error creating tape ${barcode}:`, err);
failed++;
}
}
closeModal('bulk-create-tape-modal');
e.target.reset();
// Wait a bit for service restart and refresh
setTimeout(() => {
loadVTLTapes();
refreshVTLStatus();
}, 2000);
alert(`Bulk create completed: ${success} created, ${failed} failed`);
}
// Delete Tape
async function deleteTape(barcode) {
if (!confirm(`Are you sure you want to delete tape "${barcode}"?`)) return;
try {
const res = await fetch(`/api/v1/vtl/tapes/${barcode}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (res.ok) {
loadVTLTapes();
refreshVTLStatus();
alert('Tape deleted successfully');
} else {
const err = await res.json().catch(() => ({ error: 'Failed to delete tape' }));
alert(`Error: ${err.error || err.message || 'Failed to delete tape'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
// Bulk Delete Tapes
async function bulkDeleteTapes() {
if (!confirm('Are you sure you want to delete ALL tapes? This action cannot be undone!')) return;
try {
const res = await fetch('/api/v1/vtl/tapes', { headers: getAuthHeaders() });
const tapes = await res.json();
if (!Array.isArray(tapes) || tapes.length === 0) {
alert('No tapes to delete');
return;
}
let deleted = 0;
let failed = 0;
for (const tape of tapes) {
try {
const delRes = await fetch(`/api/v1/vtl/tapes/${tape.barcode}`, {
method: 'DELETE',
headers: getAuthHeaders()
});
if (delRes.ok) {
deleted++;
} else {
failed++;
}
} catch (err) {
failed++;
}
}
loadVTLTapes();
refreshVTLStatus();
alert(`Bulk delete completed: ${deleted} deleted, ${failed} failed`);
} catch (err) {
alert(`Error: ${err.message}`);
}
}
// Service Control
function showServiceControl() {
refreshVTLStatus().then(() => {
const statusEl = document.getElementById('service-status-badge');
const status = document.getElementById('vtl-service-status').textContent;
statusEl.textContent = status;
statusEl.className = status === 'Running'
? 'px-3 py-1 rounded-full text-xs font-medium bg-green-900 text-green-300'
: 'px-3 py-1 rounded-full text-xs font-medium bg-red-900 text-red-300';
document.getElementById('service-control-modal').classList.remove('hidden');
});
}
async function controlVTLService(action) {
if (!confirm(`Are you sure you want to ${action} the mhvtl service?`)) return;
try {
const res = await fetch('/api/v1/vtl/service', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ action: action })
});
if (res.ok) {
setTimeout(() => {
refreshVTLStatus();
closeModal('service-control-modal');
alert(`Service ${action}ed successfully`);
}, 1000);
} else {
const err = await res.json().catch(() => ({ error: `Failed to ${action} service` }));
alert(`Error: ${err.error || err.message || `Failed to ${action} service`}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
// Tape Operations
async function loadTape(driveId) {
const barcode = prompt('Enter tape barcode to load:');
if (!barcode) return;
try {
const res = await fetch('/api/v1/vtl/tape/load', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
drive_id: driveId,
barcode: barcode
})
});
if (res.ok) {
loadVTLDrives();
refreshVTLStatus();
alert('Tape loaded successfully');
} else {
const err = await res.json().catch(() => ({ error: 'Failed to load tape' }));
alert(`Error: ${err.error || err.message || 'Failed to load tape'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
async function ejectTape(driveId) {
if (!confirm(`Are you sure you want to eject the tape from drive ${driveId}?`)) return;
try {
const res = await fetch('/api/v1/vtl/tape/eject', {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
drive_id: driveId
})
});
if (res.ok) {
loadVTLDrives();
refreshVTLStatus();
alert('Tape ejected successfully');
} else {
const err = await res.json().catch(() => ({ error: 'Failed to eject tape' }));
alert(`Error: ${err.error || err.message || 'Failed to eject tape'}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
function closeModal(modalId) {
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 {
const res = await fetch('/api/v1/vtl/changer/status', { headers: getAuthHeaders() });
if (!res.ok) {
document.getElementById('vtl-changer-status').textContent = 'N/A';
document.getElementById('vtl-changer-detail').textContent = 'No Libraries';
return;
}
const changers = await res.json();
const changerList = Array.isArray(changers) ? changers : [changers];
if (changerList.length === 0) {
document.getElementById('vtl-changer-status').textContent = 'None';
document.getElementById('vtl-changer-detail').textContent = 'No Libraries';
return;
}
// Show count of online changers
const onlineCount = changerList.filter(c => c.status === 'online').length;
const totalCount = changerList.length;
document.getElementById('vtl-changer-status').textContent = `${onlineCount}/${totalCount}`;
document.getElementById('vtl-changer-detail').textContent = `${totalCount} Libraries`;
} catch (err) {
document.getElementById('vtl-changer-status').textContent = 'Error';
document.getElementById('vtl-changer-detail').textContent = 'Error';
}
}
// Initial load
refreshVTLStatus();
loadVTLDrives();
// Auto-refresh every 1 minute (60000 ms)
setInterval(() => {
console.log('Auto-refreshing VTL status...');
refreshVTLStatus();
// Refresh current tab content
if (currentVTLTab === 'drives') {
loadVTLDrives();
} else if (currentVTLTab === 'tapes') {
loadVTLTapes();
} else if (currentVTLTab === 'changer') {
loadMediaChanger();
}
}, 60000); // 1 minute
</script>
{{end}}
{{define "vtl.html"}}
{{template "base" .}}
{{end}}