/** * MHVTL Configuration Web UI * * IMPORTANT: MHVTL Drive ID Convention * ------------------------------------- * Drive IDs must follow the format: Library ID (tens digit) + Slot Number (ones digit) * * Examples for Library 10: * - Slot 1 → Drive ID 11 (10 + 1) * - Slot 2 → Drive ID 12 (10 + 2) * - Slot 3 → Drive ID 13 (10 + 3) * - Slot 4 → Drive ID 14 (10 + 4) * * Examples for Library 30: * - Slot 1 → Drive ID 31 (30 + 1) * - Slot 2 → Drive ID 32 (30 + 2) * * This convention is enforced throughout the UI to ensure compatibility with mhvtl. */ let drives = []; let driveCounter = 0; let currentUser = null; const driveTypes = { 'IBM ULT3580-TD5': { vendor: 'IBM', product: 'ULT3580-TD5', type: 'LTO-5' }, 'IBM ULT3580-TD6': { vendor: 'IBM', product: 'ULT3580-TD6', type: 'LTO-6' }, 'IBM ULT3580-TD7': { vendor: 'IBM', product: 'ULT3580-TD7', type: 'LTO-7' }, 'IBM ULT3580-TD8': { vendor: 'IBM', product: 'ULT3580-TD8', type: 'LTO-8' }, 'IBM ULT3580-TD9': { vendor: 'IBM', product: 'ULT3580-TD9', type: 'LTO-9' }, 'HP Ultrium 5-SCSI': { vendor: 'HP', product: 'Ultrium 5-SCSI', type: 'LTO-5' }, 'HP Ultrium 6-SCSI': { vendor: 'HP', product: 'Ultrium 6-SCSI', type: 'LTO-6' }, }; document.addEventListener('DOMContentLoaded', function () { checkSession(); }); // Check if user is logged in async function checkSession() { try { const response = await fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'check_session' }) }); const data = await response.json(); if (!data.success || !data.logged_in) { // Not logged in, redirect to login page window.location.href = 'login.html'; return; } // User is logged in currentUser = data.user; initializeApp(); } catch (error) { console.error('Session check failed:', error); window.location.href = 'login.html'; } } // Initialize application after successful authentication function initializeApp() { initNavigation(); loadExistingConfig(); loadSystemHealth(); updateUserInfo(); applyRoleBasedUI(); } function initNavigation() { const navLinks = document.querySelectorAll('.nav-link'); navLinks.forEach(link => { link.addEventListener('click', function (e) { e.preventDefault(); const targetId = this.getAttribute('href').substring(1); document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active')); document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); this.classList.add('active'); document.getElementById(targetId).classList.add('active'); // Auto reload for Viz if (targetId === 'manage-tapes') { loadLibraryStatus(); } }); }); } function addDefaultDrives() { for (let i = 0; i < 4; i++) { addDrive(i < 2 ? 'IBM ULT3580-TD5' : 'IBM ULT3580-TD6'); } } /** * Load existing configuration from server * Parses device.conf and populates the UI */ function loadExistingConfig() { fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'load_config' }) }) .then(response => response.json()) .then(data => { if (data.success && data.config) { parseAndLoadConfig(data.config); } else { // If no config exists, use defaults console.log('No existing config found, using defaults'); addDefaultDrives(); generateConfig(); } }) .catch(error => { console.error('Error loading config:', error); // Fallback to defaults on error addDefaultDrives(); generateConfig(); }); } /** * Parse device.conf content and populate UI */ function parseAndLoadConfig(configText) { const lines = configText.split('\n'); let currentSection = null; let libraryData = {}; let drivesData = []; let currentDrive = null; for (let line of lines) { line = line.trim(); // Parse Library section if (line.startsWith('Library:')) { const match = line.match(/Library:\s+(\d+)\s+CHANNEL:\s+(\d+)\s+TARGET:\s+(\d+)\s+LUN:\s+(\d+)/); if (match) { libraryData.id = match[1]; libraryData.channel = match[2]; libraryData.target = match[3]; libraryData.lun = match[4]; currentSection = 'library'; } } // Parse Drive section else if (line.startsWith('Drive:')) { // Save previous drive if exists if (currentDrive) { drivesData.push(currentDrive); } const match = line.match(/Drive:\s+(\d+)\s+CHANNEL:\s+(\d+)\s+TARGET:\s+(\d+)\s+LUN:\s+(\d+)/); if (match) { currentDrive = { driveNum: parseInt(match[1]), channel: parseInt(match[2]), target: parseInt(match[3]), lun: parseInt(match[4]) }; currentSection = 'drive'; } } // Parse library properties else if (currentSection === 'library') { if (line.includes('Vendor identification:')) { libraryData.vendor = line.split(':')[1].trim(); } else if (line.includes('Product identification:')) { libraryData.product = line.split(':')[1].trim(); } else if (line.includes('Unit serial number:')) { libraryData.serial = line.split(':')[1].trim(); } else if (line.includes('NAA:')) { libraryData.naa = line.split(':').slice(1).join(':').trim(); } else if (line.includes('Home directory:')) { libraryData.home = line.split(':')[1].trim(); } else if (line.includes('Backoff:')) { libraryData.backoff = line.split(':')[1].trim(); } } // Parse drive properties else if (currentSection === 'drive' && currentDrive) { if (line.includes('Library ID:')) { const match = line.match(/Library ID:\s+(\d+)\s+Slot:\s+(\d+)/); if (match) { currentDrive.libraryId = parseInt(match[1]); currentDrive.slot = parseInt(match[2]); } } else if (line.includes('Vendor identification:')) { currentDrive.vendor = line.split(':')[1].trim(); } else if (line.includes('Product identification:')) { currentDrive.product = line.split(':')[1].trim(); // Determine drive type from product currentDrive.type = findDriveType(currentDrive.vendor, currentDrive.product); } else if (line.includes('Unit serial number:')) { currentDrive.serial = line.split(':')[1].trim(); } else if (line.includes('NAA:')) { currentDrive.naa = line.split(':').slice(1).join(':').trim(); } else if (line.includes('Compression: factor')) { const match = line.match(/factor\s+(\d+)\s+enabled\s+(\d+)/); if (match) { currentDrive.compression = parseInt(match[1]); currentDrive.compressionEnabled = parseInt(match[2]); } } else if (line.includes('Compression type:')) { currentDrive.compressionType = line.split(':')[1].trim(); } else if (line.includes('Backoff:')) { currentDrive.backoff = parseInt(line.split(':')[1].trim()); } } } // Save last drive if (currentDrive) { drivesData.push(currentDrive); } // Populate library fields if (libraryData.id) { document.getElementById('lib-id').value = libraryData.id || '10'; document.getElementById('lib-channel').value = libraryData.channel || '0'; document.getElementById('lib-target').value = libraryData.target || '0'; document.getElementById('lib-lun').value = libraryData.lun || '0'; document.getElementById('lib-vendor').value = libraryData.vendor || 'ADASTRA'; document.getElementById('lib-product').value = libraryData.product || 'HEPHAESTUS-V'; document.getElementById('lib-serial').value = libraryData.serial || 'HPV00001'; document.getElementById('lib-naa').value = libraryData.naa || '10:22:33:44:ab:cd:ef:00'; document.getElementById('lib-home').value = libraryData.home || '/opt/mhvtl'; document.getElementById('lib-backoff').value = libraryData.backoff || '400'; } // Populate drives drives = []; driveCounter = 0; if (drivesData.length > 0) { drivesData.forEach(driveData => { const driveId = driveCounter++; const drive = { id: driveId, driveNum: driveData.driveNum, channel: driveData.channel, target: driveData.target, lun: driveData.lun, libraryId: driveData.libraryId || 10, slot: driveData.slot || 1, type: driveData.type || 'IBM ULT3580-TD8', serial: driveData.serial || `XYZZY_A${driveData.slot}`, naa: driveData.naa || `10:22:33:44:ab:cd:ef:0${driveData.slot}`, compression: driveData.compression || 3, compressionEnabled: driveData.compressionEnabled || 1, compressionType: driveData.compressionType || 'lzo', backoff: driveData.backoff || 400 }; drives.push(drive); renderDrive(drive); }); } else { // No drives found, add defaults addDefaultDrives(); } // Generate config preview generateConfig(); } /** * Find drive type from vendor and product */ function findDriveType(vendor, product) { for (const [key, value] of Object.entries(driveTypes)) { if (value.vendor === vendor && value.product === product) { return key; } } // Default fallback return 'IBM ULT3580-TD8'; } function addDrive(driveType = 'IBM ULT3580-TD5') { const driveId = driveCounter++; const slot = drives.length + 1; const libraryId = 10; // MHVTL Convention: Drive ID = Library ID (tens digit) + Slot (ones digit) // For Library 10: Slot 1 = Drive 11, Slot 2 = Drive 12, etc. const driveNum = libraryId + slot; const drive = { id: driveId, driveNum: driveNum, channel: 0, target: drives.length + 1, lun: 0, libraryId: libraryId, slot: slot, type: driveType, serial: `XYZZY_A${slot}`, naa: `10:22:33:44:ab:cd:ef:0${slot}`, compression: 3, compressionEnabled: 1, compressionType: 'lzo', backoff: 400 }; drives.push(drive); renderDrive(drive); } function renderDrive(drive) { const container = document.getElementById('drives-container'); const driveInfo = driveTypes[drive.type]; const driveCard = document.createElement('div'); driveCard.className = 'drive-card'; driveCard.id = `drive-${drive.id}`; driveCard.innerHTML = `

💾 Drive ${drive.driveNum}

`; container.appendChild(driveCard); } function updateDrive(driveId, field, value) { const drive = drives.find(d => d.id === driveId); if (drive) { drive[field] = value; // Recalculate drive number if library ID or slot changes // MHVTL Convention: Drive ID = Library ID + Slot if (field === 'libraryId' || field === 'slot') { drive.driveNum = drive.libraryId + drive.slot; // Re-render to update the display document.getElementById('drives-container').innerHTML = ''; drives.forEach(d => renderDrive(d)); } } } function updateDriveType(driveId, type) { const drive = drives.find(d => d.id === driveId); if (drive) { drive.type = type; } } function removeDrive(driveId) { const index = drives.findIndex(d => d.id === driveId); if (index !== -1) { drives.splice(index, 1); document.getElementById(`drive-${driveId}`).remove(); // Recalculate drive numbers and slots using MHVTL convention drives.forEach((drive, idx) => { const slot = idx + 1; drive.slot = slot; drive.driveNum = drive.libraryId + slot; }); document.getElementById('drives-container').innerHTML = ''; drives.forEach(drive => renderDrive(drive)); } } function generateConfig() { const libId = document.getElementById('lib-id').value; const libChannel = document.getElementById('lib-channel').value; const libTarget = document.getElementById('lib-target').value; const libLun = document.getElementById('lib-lun').value; const libVendor = document.getElementById('lib-vendor').value; const libProduct = document.getElementById('lib-product').value; const libSerial = document.getElementById('lib-serial').value; const libNaa = document.getElementById('lib-naa').value; const libHome = document.getElementById('lib-home').value; const libBackoff = document.getElementById('lib-backoff').value; let config = `VERSION: 5\n\n`; config += `Library: ${libId} CHANNEL: ${libChannel.padStart(2, '0')} TARGET: ${libTarget.padStart(2, '0')} LUN: ${libLun.padStart(2, '0')}\n`; config += ` Vendor identification: ${libVendor}\n`; config += ` Product identification: ${libProduct}\n`; config += ` Unit serial number: ${libSerial}\n`; config += ` NAA: ${libNaa}\n`; config += ` Home directory: ${libHome}\n`; config += ` Backoff: ${libBackoff}\n`; drives.forEach(drive => { const driveInfo = driveTypes[drive.type]; config += `\nDrive: ${drive.driveNum.toString().padStart(2, '0')} CHANNEL: ${drive.channel.toString().padStart(2, '0')} TARGET: ${drive.target.toString().padStart(2, '0')} LUN: ${drive.lun.toString().padStart(2, '0')}\n`; config += ` Library ID: ${drive.libraryId} Slot: ${drive.slot.toString().padStart(2, '0')}\n`; config += ` Vendor identification: ${driveInfo.vendor}\n`; config += ` Product identification: ${driveInfo.product}\n`; config += ` Unit serial number: ${drive.serial}\n`; config += ` NAA: ${drive.naa}\n`; config += ` Compression: factor ${drive.compression} enabled ${drive.compressionEnabled}\n`; config += ` Compression type: ${drive.compressionType}\n`; config += ` Backoff: ${drive.backoff}\n`; }); document.getElementById('config-preview').textContent = config; const tapeLibrary = document.getElementById('tape-library').value; const tapeBarcodePrefix = document.getElementById('tape-barcode-prefix').value; const tapeStartNum = parseInt(document.getElementById('tape-start-num').value); const tapeSize = document.getElementById('tape-size').value; const tapeMediaType = document.getElementById('tape-media-type').value; const tapeDensity = document.getElementById('tape-density').value; const tapeCount = parseInt(document.getElementById('tape-count').value); let installCmds = '#!/bin/bash\n'; installCmds += '# Generate virtual tapes for mhvtl\n'; installCmds += '# Run this script after mhvtl installation\n\n'; for (let i = 0; i < tapeCount; i++) { const barcodeNum = String(tapeStartNum + i).padStart(6, '0'); const barcode = `${tapeBarcodePrefix}${barcodeNum}`; installCmds += `mktape -l ${tapeLibrary} -m ${barcode} -s ${tapeSize} -t ${tapeMediaType} -d ${tapeDensity}\n`; } document.getElementById('install-command').textContent = installCmds; } function downloadConfig() { generateConfig(); const config = document.getElementById('config-preview').textContent; const blob = new Blob([config], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'device.conf'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification('Configuration downloaded successfully!', 'success'); } function copyConfig() { generateConfig(); const config = document.getElementById('config-preview').textContent; navigator.clipboard.writeText(config).then(() => { showNotification('Configuration copied to clipboard!', 'success'); }).catch(() => { showNotification('Failed to copy configuration', 'danger'); }); } function copyInstallCommand() { const cmd = document.getElementById('install-command').textContent; navigator.clipboard.writeText(cmd).then(() => { showNotification('Command copied to clipboard!', 'success'); }).catch(() => { showNotification('Failed to copy command', 'danger'); }); } function generateConfigText() { const libId = document.getElementById('lib-id').value; const libChannel = document.getElementById('lib-channel').value; const libTarget = document.getElementById('lib-target').value; const libLun = document.getElementById('lib-lun').value; const libVendor = document.getElementById('lib-vendor').value; const libProduct = document.getElementById('lib-product').value; const libSerial = document.getElementById('lib-serial').value; const libNaa = document.getElementById('lib-naa').value; const libHome = document.getElementById('lib-home').value; const libBackoff = document.getElementById('lib-backoff').value; let config = `VERSION: 5\n\n`; config += `Library: ${libId} CHANNEL: ${libChannel.padStart(2, '0')} TARGET: ${libTarget.padStart(2, '0')} LUN: ${libLun.padStart(2, '0')}\n`; config += ` Vendor identification: ${libVendor}\n`; config += ` Product identification: ${libProduct}\n`; config += ` Unit serial number: ${libSerial}\n`; config += ` NAA: ${libNaa}\n`; config += ` Home directory: ${libHome}\n`; config += ` Backoff: ${libBackoff}\n`; drives.forEach(drive => { const driveInfo = driveTypes[drive.type]; config += `\nDrive: ${drive.driveNum.toString().padStart(2, '0')} CHANNEL: ${drive.channel.toString().padStart(2, '0')} TARGET: ${drive.target.toString().padStart(2, '0')} LUN: ${drive.lun.toString().padStart(2, '0')}\n`; config += ` Library ID: ${drive.libraryId} Slot: ${drive.slot.toString().padStart(2, '0')}\n`; config += ` Vendor identification: ${driveInfo.vendor}\n`; config += ` Product identification: ${driveInfo.product}\n`; config += ` Unit serial number: ${drive.serial}\n`; config += ` NAA: ${drive.naa}\n`; config += ` Compression: factor ${drive.compression} enabled ${drive.compressionEnabled}\n`; config += ` Compression type: ${drive.compressionType}\n`; config += ` Backoff: ${drive.backoff}\n`; }); return config; } function applyConfig() { const config = generateConfigText(); const resultDiv = document.getElementById('apply-result'); resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-info'; resultDiv.innerHTML = ' Applying configuration to server...'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'save_config', config: config }) }) .then(response => response.json()) .then(data => { if (data.success) { resultDiv.className = 'alert alert-success'; resultDiv.innerHTML = ` ✅ Success! Configuration saved to ${data.file}
Restart mhvtl service to apply changes using the button below. `; showNotification('Configuration applied successfully!', 'success'); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; showNotification('Failed to apply configuration', 'danger'); } }) .catch(error => { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; showNotification('Failed to apply configuration', 'danger'); }); } function restartService() { const resultDiv = document.getElementById('restart-result'); resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-info'; resultDiv.innerHTML = ' Restarting mhvtl service...'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'restart_service' }) }) .then(response => response.json()) .then(data => { if (data.success) { resultDiv.className = 'alert alert-success'; resultDiv.innerHTML = '✅ Success! Service restarted successfully'; showNotification('Service restarted successfully!', 'success'); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; showNotification('Failed to restart service', 'danger'); } }) .catch(error => { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; showNotification('Failed to restart service', 'danger'); }); } function showNotification(message, type) { const notification = document.createElement('div'); notification.className = `alert alert-${type}`; notification.style.position = 'fixed'; notification.style.top = '20px'; notification.style.right = '20px'; notification.style.zIndex = '9999'; notification.style.minWidth = '300px'; notification.style.animation = 'slideIn 0.3s ease'; notification.innerHTML = `${type === 'success' ? '✅' : '❌'} ${message}`; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease'; setTimeout(() => { document.body.removeChild(notification); }, 300); }, 3000); } const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } } `; document.head.appendChild(style); let tapeListCache = []; function loadTapeList() { const loadingDiv = document.getElementById('tape-list-loading'); const errorDiv = document.getElementById('tape-list-error'); const emptyDiv = document.getElementById('tape-list-empty'); const containerDiv = document.getElementById('tape-list-container'); loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; emptyDiv.style.display = 'none'; containerDiv.style.display = 'none'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'list_tapes' }) }) .then(response => response.json()) .then(data => { loadingDiv.style.display = 'none'; if (data.success) { tapeListCache = data.tapes; if (data.tapes.length === 0) { emptyDiv.style.display = 'block'; } else { containerDiv.style.display = 'block'; renderTapeList(data.tapes); } } else { errorDiv.style.display = 'block'; errorDiv.innerHTML = `❌ Error: ${data.error}`; } }) .catch(error => { loadingDiv.style.display = 'none'; errorDiv.style.display = 'block'; errorDiv.innerHTML = `❌ Error: ${error.message}`; }); } function renderTapeList(tapes) { const tbody = document.getElementById('tape-list-body'); const countDisplay = document.getElementById('tape-count-display'); tbody.innerHTML = ''; countDisplay.textContent = tapes.length; tapes.forEach(tape => { const row = document.createElement('tr'); row.innerHTML = ` ${tape.name} ${tape.size} ${tape.modified} `; tbody.appendChild(row); }); } function filterTapes() { const searchTerm = document.getElementById('tape-search').value.toLowerCase(); const filteredTapes = tapeListCache.filter(tape => tape.name.toLowerCase().includes(searchTerm) ); renderTapeList(filteredTapes); } function deleteTape(tapeName) { if (!confirm(`Are you sure you want to delete tape "${tapeName}"?\n\nThis action cannot be undone!`)) { return; } fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'delete_tape', tape_name: tapeName }) }) .then(response => response.json()) .then(data => { if (data.success) { showNotification(`Tape "${tapeName}" deleted successfully!`, 'success'); loadTapeList(); } else { showNotification(`Failed to delete tape: ${data.error}`, 'danger'); } }) .catch(error => { showNotification(`Error: ${error.message}`, 'danger'); }); } function bulkDeleteTapes() { const pattern = document.getElementById('bulk-delete-pattern').value.trim(); const resultDiv = document.getElementById('bulk-delete-result'); if (!pattern) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: Please enter a delete pattern'; return; } if (!confirm(`Are you sure you want to delete all tapes matching "${pattern}"?\n\nThis action cannot be undone!`)) { return; } resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-info'; resultDiv.innerHTML = ' Deleting tapes...'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'bulk_delete_tapes', pattern: pattern }) }) .then(response => response.json()) .then(data => { if (data.success) { resultDiv.className = 'alert alert-success'; resultDiv.innerHTML = `✅ Success! Deleted ${data.deleted_count} tape(s)`; showNotification(`Deleted ${data.deleted_count} tape(s)`, 'success'); loadTapeList(); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; } }) .catch(error => { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; }); } function createTapes() { const library = document.getElementById('create-library').value; const barcodePrefix = document.getElementById('create-barcode-prefix').value.trim(); const startNum = parseInt(document.getElementById('create-start-num').value); const count = parseInt(document.getElementById('create-count').value); const size = document.getElementById('create-size').value; const mediaType = document.getElementById('create-media-type').value; const density = document.getElementById('create-density').value; const resultDiv = document.getElementById('create-result'); if (!barcodePrefix) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: Barcode prefix is required'; return; } if (count < 1 || count > 100) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: Number of tapes must be between 1 and 100'; return; } resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-info'; resultDiv.innerHTML = ' Creating tapes...'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'create_tapes', library: library, barcode_prefix: barcodePrefix, start_num: startNum, count: count, size: size, media_type: mediaType, density: density }) }) .then(response => response.json()) .then(data => { if (data.success) { resultDiv.className = 'alert alert-success'; resultDiv.innerHTML = `✅ Success! Created ${data.created_count} tape(s)`; if (data.errors && data.errors.length > 0) { resultDiv.innerHTML += `
Errors: ${data.errors.join(', ')}`; } showNotification(`Created ${data.created_count} tape(s)`, 'success'); loadTapeList(); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; } }) .catch(error => { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; }); } // ============================================ // iSCSI Target Management Functions // ============================================ function loadDeviceMapping() { const loading = document.getElementById('device-mapping-loading'); const mappingDiv = document.getElementById('device-mapping'); if (loading) loading.style.display = 'block'; if (mappingDiv) mappingDiv.innerHTML = ''; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'device_mapping' }) }) .then(response => response.json()) .then(data => { if (loading) loading.style.display = 'none'; if (data.success) { if (data.devices.length === 0) { mappingDiv.innerHTML = '
No VTL devices found via lsscsi.
'; return; } let html = ` `; data.devices.forEach(dev => { html += ` `; }); html += `
SCSI ID Type Vendor/Product Device Path
${dev.scsi_id} ${dev.type} ${dev.vendor} ${dev.model} ${dev.dev_path}

ℹ️ Use the Device Path (e.g. /dev/sgX) when adding LUNs to iSCSI targets.

`; mappingDiv.innerHTML = html; } else { mappingDiv.innerHTML = `
❌ Error: ${data.error}
`; } }) .catch(error => { if (loading) loading.style.display = 'none'; if (mappingDiv) mappingDiv.innerHTML = `
❌ Error: ${error.message}
`; }); } function loadTargets() { fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'list_targets' }) }) .then(response => response.json()) .then(data => { if (data.success) { const tbody = document.getElementById('target-list-body'); const emptyDiv = document.getElementById('target-list-empty'); const containerDiv = document.getElementById('target-list-container'); if (data.targets.length === 0) { emptyDiv.style.display = 'block'; containerDiv.style.display = 'none'; } else { emptyDiv.style.display = 'none'; containerDiv.style.display = 'block'; tbody.innerHTML = data.targets.map(target => ` ${target.tid} ${target.name} ${target.luns || 0} ${target.acls || 0} `).join(''); } } else { showNotification(data.error, 'error'); } }) .catch(error => { showNotification('Failed to load targets: ' + error.message, 'error'); }); } function createTarget() { const tid = document.getElementById('target-tid').value; const name = document.getElementById('target-name').value.trim(); const resultDiv = document.getElementById('create-target-result'); if (!name) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: Target name is required'; return; } resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-info'; resultDiv.innerHTML = ' Creating target...'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'create_target', tid: tid, name: name }) }) .then(response => response.json()) .then(data => { if (data.success) { resultDiv.className = 'alert alert-success'; resultDiv.innerHTML = `✅ Success! Target created: ${data.iqn}`; showNotification('Target created successfully', 'success'); loadTargets(); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; } }) .catch(error => { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; }); } function deleteTarget(tid) { if (!confirm(`Delete target ${tid}? This will remove all LUNs and ACLs.`)) { return; } fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'delete_target', tid: tid }) }) .then(response => response.json()) .then(data => { if (data.success) { showNotification('Target deleted successfully', 'success'); loadTargets(); } else { showNotification(data.error, 'error'); } }) .catch(error => { showNotification('Failed to delete target: ' + error.message, 'error'); }); } function addLun() { const tid = document.getElementById('lun-tid').value; const lun = document.getElementById('lun-number').value; const device = document.getElementById('lun-device').value.trim(); const resultDiv = document.getElementById('add-lun-result'); if (!device) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: Device path is required'; return; } resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-info'; resultDiv.innerHTML = ' Adding LUN...'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'add_lun', tid: tid, lun: lun, device: device }) }) .then(response => response.json()) .then(data => { if (data.success) { resultDiv.className = 'alert alert-success'; resultDiv.innerHTML = '✅ Success! LUN added successfully'; showNotification('LUN added successfully', 'success'); loadTargets(); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; } }) .catch(error => { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; }); } function bindInitiator() { const tid = document.getElementById('acl-tid').value; const address = document.getElementById('acl-address').value.trim(); const resultDiv = document.getElementById('acl-result'); if (!address) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: Initiator address is required'; return; } resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-info'; resultDiv.innerHTML = ' Binding initiator...'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'bind_initiator', tid: tid, address: address }) }) .then(response => response.json()) .then(data => { if (data.success) { resultDiv.className = 'alert alert-success'; resultDiv.innerHTML = '✅ Success! Initiator allowed'; showNotification('Initiator allowed successfully', 'success'); loadTargets(); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; } }) .catch(error => { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; }); } function unbindInitiator() { const tid = document.getElementById('acl-tid').value; const address = document.getElementById('acl-address').value.trim(); const resultDiv = document.getElementById('acl-result'); if (!address) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: Initiator address is required'; return; } if (!confirm(`Block initiator ${address} from target ${tid}?`)) { return; } resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-info'; resultDiv.innerHTML = ' Unbinding initiator...'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'unbind_initiator', tid: tid, address: address }) }) .then(response => response.json()) .then(data => { if (data.success) { resultDiv.className = 'alert alert-success'; resultDiv.innerHTML = '✅ Success! Initiator blocked'; showNotification('Initiator blocked successfully', 'success'); loadTargets(); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; } }) .catch(error => { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; }); } // ============================================ // System Health & Management Functions // ============================================ function loadSystemHealth() { refreshSystemHealth(); // Auto-refresh every 30 seconds setInterval(refreshSystemHealth, 30000); } function refreshSystemHealth() { const dashboard = document.getElementById('health-dashboard'); const loading = document.getElementById('health-loading'); loading.style.display = 'block'; dashboard.style.display = 'none'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'system_health' }) }) .then(response => response.json()) .then(data => { loading.style.display = 'none'; dashboard.style.display = 'block'; if (data.success) { renderHealthDashboard(data.health); } else { dashboard.innerHTML = `
❌ Error: ${data.error}
`; } }) .catch(error => { loading.style.display = 'none'; dashboard.style.display = 'block'; dashboard.innerHTML = `
❌ Error: ${error.message}
`; }); } function renderHealthDashboard(health) { const dashboard = document.getElementById('health-dashboard'); // Overall health status let statusClass = 'success'; let statusIcon = '✅'; if (health.overall.status === 'degraded') { statusClass = 'warning'; statusIcon = '⚠️'; } else if (health.overall.status === 'critical') { statusClass = 'danger'; statusIcon = '❌'; } let html = `

${statusIcon} System Status: ${health.overall.status.toUpperCase()}

${health.overall.message}

Health Score: ${health.overall.score}/${health.overall.total} (${health.overall.percentage}%)

Uptime: ${health.system.uptime}

📊 Services

`; for (const [name, service] of Object.entries(health.services)) { const statusIcon = service.running ? '🟢' : '🔴'; const statusText = service.running ? 'Running' : 'Stopped'; const enabledIcon = service.enabled ? '✅' : '❌'; const enabledText = service.enabled ? 'Enabled' : 'Disabled'; html += ` `; } html += `
Service Status Auto-Start
${name} ${statusIcon} ${statusText} ${enabledIcon} ${enabledText}

🔧 Components

`; // vtltape const vtltapeIcon = health.components.vtltape.running ? '🟢' : '🔴'; const vtltapeStatus = health.components.vtltape.running ? 'Running' : 'Stopped'; const vtltapeDetails = health.components.vtltape.running ? `${health.components.vtltape.count} processes` : 'N/A'; html += ` `; // vtllibrary const vtllibraryIcon = health.components.vtllibrary.running ? '🟢' : '🔴'; const vtllibraryStatus = health.components.vtllibrary.running ? 'Running' : 'Stopped'; html += ` `; html += `
Component Status Details
vtltape ${vtltapeIcon} ${vtltapeStatus} ${vtltapeDetails}
vtllibrary ${vtllibraryIcon} ${vtllibraryStatus} -

💾 SCSI Devices

`; // Library const libraryIcon = health.devices.library.detected ? '🟢' : '🔴'; const libraryStatus = health.devices.library.detected ? 'Detected' : 'Not Detected'; const libraryInfo = health.devices.library.info || 'N/A'; html += ` `; // Drives const drivesIcon = health.devices.drives.detected ? '🟢' : '🔴'; const drivesStatus = health.devices.drives.detected ? 'Detected' : 'Not Detected'; const drivesCount = health.devices.drives.count; html += ` `; html += `
Device Type Status Details
Library (Changer) ${libraryIcon} ${libraryStatus} ${libraryInfo}
Tape Drives ${drivesIcon} ${drivesStatus} ${drivesCount} drive(s)
`; // Show drive details if available if (health.devices.drives.detected && health.devices.drives.list.length > 0) { html += `
Drive Details:
${health.devices.drives.list.join('\n')}
`; } dashboard.innerHTML = html; } function restartAppliance() { if (!confirm('⚠️ Are you sure you want to restart the appliance?\n\nThis will reboot the entire system and temporarily interrupt all services.')) { return; } const resultDiv = document.getElementById('power-result'); resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-info'; resultDiv.innerHTML = ' Initiating system restart...'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'restart_appliance' }) }) .then(response => response.json()) .then(data => { if (data.success) { resultDiv.className = 'alert alert-success'; resultDiv.innerHTML = `✅ Success: ${data.message}`; // Show countdown let countdown = 60; const countdownInterval = setInterval(() => { countdown--; resultDiv.innerHTML = `🔄 Restarting... System will be back online in approximately ${countdown} seconds.`; if (countdown <= 0) { clearInterval(countdownInterval); resultDiv.innerHTML = ' System should be back online. Click here to reload'; } }, 1000); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; } }) .catch(error => { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; }); } function shutdownAppliance() { if (!confirm('⚠️ Are you sure you want to shutdown the appliance?\n\nThis will power off the entire system. You will need physical access to power it back on.')) { return; } if (!confirm('⚠️⚠️ FINAL WARNING ⚠️⚠️\n\nThis will SHUTDOWN the appliance completely.\n\nAre you absolutely sure?')) { return; } const resultDiv = document.getElementById('power-result'); resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-warning'; resultDiv.innerHTML = ' Initiating system shutdown...'; fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'shutdown_appliance' }) }) .then(response => response.json()) .then(data => { if (data.success) { resultDiv.className = 'alert alert-warning'; resultDiv.innerHTML = `⏻ Shutting Down: ${data.message}`; // Show countdown let countdown = 30; const countdownInterval = setInterval(() => { countdown--; resultDiv.innerHTML = `⏻ Shutting Down... System will power off in approximately ${countdown} seconds.`; if (countdown <= 0) { clearInterval(countdownInterval); resultDiv.innerHTML = ' System has been powered off.'; } }, 1000); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; } }) .catch(error => { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; }); } // ============================================ // User Management & Role-Based UI Functions // ============================================ function updateUserInfo() { // Add user info to navbar if element exists const navbar = document.querySelector('.nav-brand'); if (navbar && currentUser) { const userInfo = document.createElement('div'); userInfo.style.cssText = 'font-size: 0.85rem; color: #666; margin-top: 0.25rem;'; userInfo.innerHTML = ` 👤 ${currentUser.username} ${currentUser.role.toUpperCase()} Logout `; navbar.appendChild(userInfo); } } function applyRoleBasedUI() { if (!currentUser) return; const isAdmin = currentUser.role === 'admin'; // Disable admin-only buttons for viewers if (!isAdmin) { // Disable save/apply config buttons const saveButtons = document.querySelectorAll('[onclick*="applyConfig"], [onclick*="saveConfig"]'); saveButtons.forEach(btn => { btn.disabled = true; btn.title = 'Admin access required'; btn.style.opacity = '0.5'; btn.style.cursor = 'not-allowed'; }); // Disable restart/shutdown buttons const powerButtons = document.querySelectorAll('[onclick*="restartAppliance"], [onclick*="shutdownAppliance"], [onclick*="restartService"]'); powerButtons.forEach(btn => { btn.disabled = true; btn.title = 'Admin access required'; btn.style.opacity = '0.5'; btn.style.cursor = 'not-allowed'; }); // Disable create/delete tape buttons const tapeButtons = document.querySelectorAll('[onclick*="createTapes"], [onclick*="deleteTape"], [onclick*="bulkDeleteTapes"]'); tapeButtons.forEach(btn => { btn.disabled = true; btn.title = 'Admin access required'; btn.style.opacity = '0.5'; btn.style.cursor = 'not-allowed'; }); // Disable iSCSI management buttons const iscsiButtons = document.querySelectorAll('[onclick*="createTarget"], [onclick*="deleteTarget"], [onclick*="addLun"], [onclick*="bindInitiator"], [onclick*="unbindInitiator"]'); iscsiButtons.forEach(btn => { btn.disabled = true; btn.title = 'Admin access required'; btn.style.opacity = '0.5'; btn.style.cursor = 'not-allowed'; }); // Hide Users tab for viewers const usersTab = document.getElementById('users-tab'); if (usersTab) { usersTab.style.display = 'none'; } // Make form inputs readonly for viewers const inputs = document.querySelectorAll('input[type="text"], input[type="number"], select'); inputs.forEach(input => { input.setAttribute('readonly', 'readonly'); input.style.backgroundColor = '#f8f9fa'; input.style.cursor = 'not-allowed'; }); } else { // Admin - load users list loadUsers(); } } async function logout() { if (!confirm('Are you sure you want to logout?')) { return; } try { await fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'logout' }) }); // Redirect to login page window.location.href = 'login.html'; } catch (error) { console.error('Logout failed:', error); // Force redirect anyway window.location.href = 'login.html'; } } // ============================================ // User Management Functions // ============================================ async function loadUsers() { const loading = document.getElementById('users-loading'); const usersList = document.getElementById('users-list'); loading.style.display = 'block'; usersList.innerHTML = ''; try { const response = await fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'get_users' }) }); const data = await response.json(); loading.style.display = 'none'; if (data.success) { renderUsersList(data.users); } else { usersList.innerHTML = `
❌ Error: ${data.error}
`; } } catch (error) { loading.style.display = 'none'; usersList.innerHTML = `
❌ Error: ${error.message}
`; } } function renderUsersList(users) { const usersList = document.getElementById('users-list'); if (users.length === 0) { usersList.innerHTML = '
ℹ️ No users found.
'; return; } let html = ` `; users.forEach(user => { const roleClass = user.role === 'admin' ? 'success' : 'primary'; const roleBadge = `${user.role.toUpperCase()}`; const statusBadge = user.enabled ? '✅ Enabled' : '❌ Disabled'; const isCurrentUser = currentUser && currentUser.username === user.username; const deleteButton = isCurrentUser ? '' : ``; const toggleButton = user.enabled ? `` : ``; html += ` `; }); html += `
Username Role Created Status Actions
${user.username} ${roleBadge} ${user.created || 'N/A'} ${statusBadge} ${toggleButton} ${deleteButton}
`; usersList.innerHTML = html; } async function createNewUser() { const username = document.getElementById('new-username').value.trim(); const password = document.getElementById('new-password').value; const role = document.getElementById('new-role').value; const resultDiv = document.getElementById('create-user-result'); if (!username || !password) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: Username and password are required'; return; } if (password.length < 6) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: Password must be at least 6 characters'; return; } resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-info'; resultDiv.innerHTML = ' Creating user...'; try { const response = await fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'create_user', username: username, password: password, role: role }) }); const data = await response.json(); if (data.success) { resultDiv.className = 'alert alert-success'; resultDiv.innerHTML = `✅ Success: ${data.message}`; // Clear form document.getElementById('new-username').value = ''; document.getElementById('new-password').value = ''; document.getElementById('new-role').value = 'viewer'; // Reload users list setTimeout(() => { loadUsers(); resultDiv.style.display = 'none'; }, 2000); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; } } catch (error) { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; } } async function deleteUserAccount(username) { if (!confirm(`⚠️ Are you sure you want to delete user "${username}"?\n\nThis action cannot be undone.`)) { return; } try { const response = await fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'delete_user', username: username }) }); const data = await response.json(); if (data.success) { alert('✅ User deleted successfully'); loadUsers(); } else { alert('❌ Error: ' + data.error); } } catch (error) { alert('❌ Error: ' + error.message); } } async function toggleUserStatus(username, enable) { const action = enable ? 'enable' : 'disable'; if (!confirm(`Are you sure you want to ${action} user "${username}"?`)) { return; } try { const response = await fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'update_user', username: username, enabled: enable }) }); const data = await response.json(); if (data.success) { alert(`✅ User ${action}d successfully`); loadUsers(); } else { alert('❌ Error: ' + data.error); } } catch (error) { alert('❌ Error: ' + error.message); } } async function changeUserPassword() { const currentPassword = document.getElementById('current-password').value; const newPassword = document.getElementById('new-user-password').value; const confirmPassword = document.getElementById('confirm-password').value; const resultDiv = document.getElementById('change-password-result'); if (!currentPassword || !newPassword || !confirmPassword) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: All fields are required'; return; } if (newPassword !== confirmPassword) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: New passwords do not match'; return; } if (newPassword.length < 6) { resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = '❌ Error: Password must be at least 6 characters'; return; } resultDiv.style.display = 'block'; resultDiv.className = 'alert alert-info'; resultDiv.innerHTML = ' Changing password...'; try { const response = await fetch('api.php', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ action: 'change_password', current_password: currentPassword, new_password: newPassword }) }); const data = await response.json(); if (data.success) { resultDiv.className = 'alert alert-success'; resultDiv.innerHTML = `✅ Success: ${data.message}`; // Clear form document.getElementById('current-password').value = ''; document.getElementById('new-user-password').value = ''; document.getElementById('confirm-password').value = ''; setTimeout(() => { resultDiv.style.display = 'none'; }, 3000); } else { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${data.error}`; } } catch (error) { resultDiv.className = 'alert alert-danger'; resultDiv.innerHTML = `❌ Error: ${error.message}`; } } // Library Visualizer function loadLibraryStatus() { const loading = document.getElementById('viz-loading'); const container = document.getElementById('library-viz'); const errorEl = document.getElementById('viz-error'); if (!loading || !container || !errorEl) return; loading.style.display = 'block'; container.style.display = 'none'; errorEl.style.display = 'none'; fetch('api.php?action=library_status') .then(response => response.json()) .then(data => { loading.style.display = 'none'; if (data.success) { container.style.display = 'flex'; renderVizGrid('viz-drives', data.data.drives, 'drive'); renderVizGrid('viz-maps', data.data.maps, 'map'); renderVizGrid('viz-slots', data.data.slots, 'slot'); } else { errorEl.textContent = 'Error: ' + data.error; errorEl.style.display = 'block'; } }) .catch(err => { loading.style.display = 'none'; errorEl.textContent = 'Fetch Error: ' + err.message; errorEl.style.display = 'block'; }); } function renderVizGrid(containerId, items, type) { const grid = document.getElementById(containerId); if (!grid) return; grid.innerHTML = ''; if (!items || items.length === 0) { grid.innerHTML = '
No items
'; return; } items.forEach(item => { const el = document.createElement('div'); el.className = `viz-slot ${item.full ? 'full' : 'empty'} ${type === 'drive' ? 'drive-slot' : ''}`; const icon = type === 'drive' ? '💾' : (type === 'map' ? '📥' : '📼'); const label = item.full ? item.barcode : 'Empty'; el.innerHTML = ` ${icon}
${type.toUpperCase()} ${item.id}
${label}
`; grid.appendChild(el); }); }