/**
* 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 = `
`;
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 = `
| SCSI ID |
Type |
Vendor/Product |
Device Path |
`;
data.devices.forEach(dev => {
html += `
| ${dev.scsi_id} |
${dev.type} |
${dev.vendor} ${dev.model} |
${dev.dev_path} |
`;
});
html += `
ℹ️ 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
| Service |
Status |
Auto-Start |
`;
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 += `
| ${name} |
${statusIcon} ${statusText} |
${enabledIcon} ${enabledText} |
`;
}
html += `
🔧 Components
| Component |
Status |
Details |
`;
// 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 += `
| vtltape |
${vtltapeIcon} ${vtltapeStatus} |
${vtltapeDetails} |
`;
// vtllibrary
const vtllibraryIcon = health.components.vtllibrary.running ? '🟢' : '🔴';
const vtllibraryStatus = health.components.vtllibrary.running ? 'Running' : 'Stopped';
html += `
| vtllibrary |
${vtllibraryIcon} ${vtllibraryStatus} |
- |
`;
html += `
💾 SCSI Devices
| Device Type |
Status |
Details |
`;
// 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 += `
| Library (Changer) |
${libraryIcon} ${libraryStatus} |
${libraryInfo} |
`;
// Drives
const drivesIcon = health.devices.drives.detected ? '🟢' : '🔴';
const drivesStatus = health.devices.drives.detected ? 'Detected' : 'Not Detected';
const drivesCount = health.devices.drives.count;
html += `
| Tape Drives |
${drivesIcon} ${drivesStatus} |
${drivesCount} drive(s) |
`;
html += `
`;
// 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 = `
| Username |
Role |
Created |
Status |
Actions |
`;
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 += `
| ${user.username} |
${roleBadge} |
${user.created || 'N/A'} |
${statusBadge} |
${toggleButton}
${deleteButton}
|
`;
});
html += `
`;
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', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ 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);
});
}