Files
vtl-appliance/web-ui/script.js
Othman H. Suseno 01080498af feat: Major VTL System Upgrade (Auth, Monitoring, CLI, Installer)
- Web UI:
  - Added secure Authentication system (Login, 2 Roles: Admin/Viewer)
  - Added System Monitoring Dashboard (Health, Services, Power Mgmt)
  - Added User Management Interface (Create, Delete, Enable/Disable)
  - Added Device Mapping view in iSCSI tab (lsscsi output)
- Backend:
  - Implemented secure session management (auth.php)
  - Added power management APIs (restart/shutdown appliance)
  - Added device mapping API
- CLI:
  - Created global 'vtl' management tool
  - Added scripts for reliable startup (vtllibrary fix)
- Installer:
  - Updated install.sh with new dependencies (tgt, sudoers, permissions)
  - Included all new components in build-installer.sh
- Docs:
  - Consolidated documentation into docs/ folder
2025-12-09 18:15:36 +00:00

1944 lines
69 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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');
});
});
}
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 = `
<div class="drive-card-header">
<h4>💾 Drive ${drive.driveNum}</h4>
<button class="btn btn-danger" onclick="removeDrive(${drive.id})">
<span>🗑️</span> Remove
</button>
</div>
<div class="form-grid">
<div class="form-group">
<label>Drive Type</label>
<select onchange="updateDriveType(${drive.id}, this.value)">
${Object.keys(driveTypes).map(type =>
`<option value="${type}" ${type === drive.type ? 'selected' : ''}>${type}</option>`
).join('')}
</select>
</div>
<div class="form-group">
<label>Drive Number</label>
<input type="number" value="${drive.driveNum}" min="0" max="99"
onchange="updateDrive(${drive.id}, 'driveNum', parseInt(this.value))">
</div>
<div class="form-group">
<label>Channel</label>
<input type="number" value="${drive.channel}" min="0" max="15"
onchange="updateDrive(${drive.id}, 'channel', parseInt(this.value))">
</div>
<div class="form-group">
<label>Target</label>
<input type="number" value="${drive.target}" min="0" max="15"
onchange="updateDrive(${drive.id}, 'target', parseInt(this.value))">
</div>
<div class="form-group">
<label>LUN</label>
<input type="number" value="${drive.lun}" min="0" max="7"
onchange="updateDrive(${drive.id}, 'lun', parseInt(this.value))">
</div>
<div class="form-group">
<label>Library ID</label>
<input type="number" value="${drive.libraryId}" min="0" max="99"
onchange="updateDrive(${drive.id}, 'libraryId', parseInt(this.value))">
</div>
<div class="form-group">
<label>Slot</label>
<input type="number" value="${drive.slot}" min="1" max="999"
onchange="updateDrive(${drive.id}, 'slot', parseInt(this.value))">
</div>
<div class="form-group">
<label>Serial Number</label>
<input type="text" value="${drive.serial}" maxlength="10"
onchange="updateDrive(${drive.id}, 'serial', this.value)">
</div>
<div class="form-group">
<label>NAA</label>
<input type="text" value="${drive.naa}" pattern="[0-9a-f:]+"
onchange="updateDrive(${drive.id}, 'naa', this.value)">
</div>
<div class="form-group">
<label>Compression Factor</label>
<input type="number" value="${drive.compression}" min="1" max="10"
onchange="updateDrive(${drive.id}, 'compression', parseInt(this.value))">
</div>
<div class="form-group">
<label>Compression Type</label>
<select onchange="updateDrive(${drive.id}, 'compressionType', this.value)">
<option value="lzo" ${drive.compressionType === 'lzo' ? 'selected' : ''}>LZO</option>
<option value="zlib" ${drive.compressionType === 'zlib' ? 'selected' : ''}>ZLIB</option>
</select>
</div>
<div class="form-group">
<label>Backoff (ms)</label>
<input type="number" value="${drive.backoff}" min="0" max="10000"
onchange="updateDrive(${drive.id}, 'backoff', parseInt(this.value))">
</div>
</div>
`;
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 = '<strong>⏳</strong> 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 = `
<strong>✅ Success!</strong> Configuration saved to ${data.file}<br>
<small>Restart mhvtl service to apply changes using the button below.</small>
`;
showNotification('Configuration applied successfully!', 'success');
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
showNotification('Failed to apply configuration', 'danger');
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${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 = '<strong>⏳</strong> 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 = '<strong>✅ Success!</strong> Service restarted successfully';
showNotification('Service restarted successfully!', 'success');
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
showNotification('Failed to restart service', 'danger');
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${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 = `<strong>${type === 'success' ? '✅' : '❌'}</strong> ${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 = `<strong>❌ Error:</strong> ${data.error}`;
}
})
.catch(error => {
loadingDiv.style.display = 'none';
errorDiv.style.display = 'block';
errorDiv.innerHTML = `<strong>❌ Error:</strong> ${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 = `
<td class="tape-barcode">${tape.name}</td>
<td class="tape-size">${tape.size}</td>
<td class="tape-date">${tape.modified}</td>
<td class="tape-actions">
<button class="btn btn-danger btn-small" onclick="deleteTape('${tape.name}')">
<span>🗑️</span> Delete
</button>
</td>
`;
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 = '<strong>❌ Error:</strong> 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 = '<strong>⏳</strong> 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 = `<strong>✅ Success!</strong> Deleted ${data.deleted_count} tape(s)`;
showNotification(`Deleted ${data.deleted_count} tape(s)`, 'success');
loadTapeList();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${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 = '<strong>❌ Error:</strong> Barcode prefix is required';
return;
}
if (count < 1 || count > 100) {
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = '<strong>❌ Error:</strong> Number of tapes must be between 1 and 100';
return;
}
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-info';
resultDiv.innerHTML = '<strong>⏳</strong> 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 = `<strong>✅ Success!</strong> Created ${data.created_count} tape(s)`;
if (data.errors && data.errors.length > 0) {
resultDiv.innerHTML += `<br><small>Errors: ${data.errors.join(', ')}</small>`;
}
showNotification(`Created ${data.created_count} tape(s)`, 'success');
loadTapeList();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${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 = '<div class="alert alert-warning">No VTL devices found via lsscsi.</div>';
return;
}
let html = `
<table class="tape-table">
<thead>
<tr>
<th>SCSI ID</th>
<th>Type</th>
<th>Vendor/Product</th>
<th>Device Path</th>
</tr>
</thead>
<tbody>
`;
data.devices.forEach(dev => {
html += `
<tr>
<td style="font-family: monospace;">${dev.scsi_id}</td>
<td>${dev.type}</td>
<td>${dev.vendor} ${dev.model}</td>
<td style="font-family: monospace; font-weight: bold;">${dev.dev_path}</td>
</tr>
`;
});
html += `
</tbody>
</table>
<p class="help-text" style="margin-top: 10px; font-size: 0.9em; color: #666;">
Use the Device Path (e.g. <code>/dev/sgX</code>) when adding LUNs to iSCSI targets.
</p>
`;
mappingDiv.innerHTML = html;
} else {
mappingDiv.innerHTML = `<div class="alert alert-danger"><strong>❌ Error:</strong> ${data.error}</div>`;
}
})
.catch(error => {
if (loading) loading.style.display = 'none';
if (mappingDiv) mappingDiv.innerHTML = `<div class="alert alert-danger"><strong>❌ Error:</strong> ${error.message}</div>`;
});
}
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 => `
<tr>
<td><strong>${target.tid}</strong></td>
<td><code>${target.name}</code></td>
<td>${target.luns || 0}</td>
<td>${target.acls || 0}</td>
<td>
<button class="btn btn-danger btn-sm" onclick="deleteTarget(${target.tid})">
🗑️ Delete
</button>
</td>
</tr>
`).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 = '<strong>❌ Error:</strong> Target name is required';
return;
}
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-info';
resultDiv.innerHTML = '<strong>⏳</strong> 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 = `<strong>✅ Success!</strong> Target created: ${data.iqn}`;
showNotification('Target created successfully', 'success');
loadTargets();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${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 = '<strong>❌ Error:</strong> Device path is required';
return;
}
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-info';
resultDiv.innerHTML = '<strong>⏳</strong> 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 = '<strong>✅ Success!</strong> LUN added successfully';
showNotification('LUN added successfully', 'success');
loadTargets();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${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 = '<strong>❌ Error:</strong> Initiator address is required';
return;
}
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-info';
resultDiv.innerHTML = '<strong>⏳</strong> 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 = '<strong>✅ Success!</strong> Initiator allowed';
showNotification('Initiator allowed successfully', 'success');
loadTargets();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${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 = '<strong>❌ Error:</strong> 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 = '<strong>⏳</strong> 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 = '<strong>✅ Success!</strong> Initiator blocked';
showNotification('Initiator blocked successfully', 'success');
loadTargets();
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${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 = `<div class="alert alert-danger"><strong>❌ Error:</strong> ${data.error}</div>`;
}
})
.catch(error => {
loading.style.display = 'none';
dashboard.style.display = 'block';
dashboard.innerHTML = `<div class="alert alert-danger"><strong>❌ Error:</strong> ${error.message}</div>`;
});
}
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 = `
<div class="alert alert-${statusClass}" style="margin-bottom: 1.5rem;">
<h4 style="margin: 0 0 0.5rem 0;">${statusIcon} System Status: ${health.overall.status.toUpperCase()}</h4>
<p style="margin: 0;">${health.overall.message}</p>
<p style="margin: 0.5rem 0 0 0;"><strong>Health Score:</strong> ${health.overall.score}/${health.overall.total} (${health.overall.percentage}%)</p>
<p style="margin: 0.5rem 0 0 0;"><strong>Uptime:</strong> ${health.system.uptime}</p>
</div>
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">📊 Services</h4>
<table class="tape-table">
<thead>
<tr>
<th>Service</th>
<th>Status</th>
<th>Auto-Start</th>
</tr>
</thead>
<tbody>
`;
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 += `
<tr>
<td><strong>${name}</strong></td>
<td>${statusIcon} ${statusText}</td>
<td>${enabledIcon} ${enabledText}</td>
</tr>
`;
}
html += `
</tbody>
</table>
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">🔧 Components</h4>
<table class="tape-table">
<thead>
<tr>
<th>Component</th>
<th>Status</th>
<th>Details</th>
</tr>
</thead>
<tbody>
`;
// 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 += `
<tr>
<td><strong>vtltape</strong></td>
<td>${vtltapeIcon} ${vtltapeStatus}</td>
<td>${vtltapeDetails}</td>
</tr>
`;
// vtllibrary
const vtllibraryIcon = health.components.vtllibrary.running ? '🟢' : '🔴';
const vtllibraryStatus = health.components.vtllibrary.running ? 'Running' : 'Stopped';
html += `
<tr>
<td><strong>vtllibrary</strong></td>
<td>${vtllibraryIcon} ${vtllibraryStatus}</td>
<td>-</td>
</tr>
`;
html += `
</tbody>
</table>
<h4 style="margin-top: 1.5rem; margin-bottom: 1rem;">💾 SCSI Devices</h4>
<table class="tape-table">
<thead>
<tr>
<th>Device Type</th>
<th>Status</th>
<th>Details</th>
</tr>
</thead>
<tbody>
`;
// 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 += `
<tr>
<td><strong>Library (Changer)</strong></td>
<td>${libraryIcon} ${libraryStatus}</td>
<td style="font-family: monospace; font-size: 0.85em;">${libraryInfo}</td>
</tr>
`;
// Drives
const drivesIcon = health.devices.drives.detected ? '🟢' : '🔴';
const drivesStatus = health.devices.drives.detected ? 'Detected' : 'Not Detected';
const drivesCount = health.devices.drives.count;
html += `
<tr>
<td><strong>Tape Drives</strong></td>
<td>${drivesIcon} ${drivesStatus}</td>
<td>${drivesCount} drive(s)</td>
</tr>
`;
html += `
</tbody>
</table>
`;
// Show drive details if available
if (health.devices.drives.detected && health.devices.drives.list.length > 0) {
html += `
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 4px;">
<strong>Drive Details:</strong>
<pre style="margin-top: 0.5rem; font-size: 0.85em; overflow-x: auto;">${health.devices.drives.list.join('\n')}</pre>
</div>
`;
}
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 = '<strong>⏳</strong> 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 = `<strong>✅ Success:</strong> ${data.message}`;
// Show countdown
let countdown = 60;
const countdownInterval = setInterval(() => {
countdown--;
resultDiv.innerHTML = `<strong>🔄 Restarting...</strong> System will be back online in approximately ${countdown} seconds.`;
if (countdown <= 0) {
clearInterval(countdownInterval);
resultDiv.innerHTML = '<strong>✅</strong> System should be back online. <a href="javascript:location.reload()">Click here to reload</a>';
}
}, 1000);
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${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 = '<strong>⏳</strong> 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 = `<strong>⏻ Shutting Down:</strong> ${data.message}`;
// Show countdown
let countdown = 30;
const countdownInterval = setInterval(() => {
countdown--;
resultDiv.innerHTML = `<strong>⏻ Shutting Down...</strong> System will power off in approximately ${countdown} seconds.`;
if (countdown <= 0) {
clearInterval(countdownInterval);
resultDiv.innerHTML = '<strong>⏻</strong> System has been powered off.';
}
}, 1000);
} else {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${data.error}`;
}
})
.catch(error => {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${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}
<span style="background: ${currentUser.role === 'admin' ? '#28a745' : '#007bff'}; color: white; padding: 0.125rem 0.5rem; border-radius: 12px; font-size: 0.75rem; margin-left: 0.5rem;">
${currentUser.role.toUpperCase()}
</span>
<a href="javascript:logout()" style="margin-left: 1rem; color: #dc3545; text-decoration: none;">Logout</a>
`;
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 = `<div class="alert alert-danger"><strong>❌ Error:</strong> ${data.error}</div>`;
}
} catch (error) {
loading.style.display = 'none';
usersList.innerHTML = `<div class="alert alert-danger"><strong>❌ Error:</strong> ${error.message}</div>`;
}
}
function renderUsersList(users) {
const usersList = document.getElementById('users-list');
if (users.length === 0) {
usersList.innerHTML = '<div class="alert alert-info"><strong></strong> No users found.</div>';
return;
}
let html = `
<table class="tape-table">
<thead>
<tr>
<th>Username</th>
<th>Role</th>
<th>Created</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
`;
users.forEach(user => {
const roleClass = user.role === 'admin' ? 'success' : 'primary';
const roleBadge = `<span style="background: ${user.role === 'admin' ? '#28a745' : '#007bff'}; color: white; padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.85rem;">${user.role.toUpperCase()}</span>`;
const statusBadge = user.enabled
? '<span style="color: #28a745;">✅ Enabled</span>'
: '<span style="color: #dc3545;">❌ Disabled</span>';
const isCurrentUser = currentUser && currentUser.username === user.username;
const deleteButton = isCurrentUser
? '<button class="btn btn-secondary" disabled title="Cannot delete your own account">🗑️ Delete</button>'
: `<button class="btn btn-danger" onclick="deleteUserAccount('${user.username}')">🗑️ Delete</button>`;
const toggleButton = user.enabled
? `<button class="btn btn-warning" onclick="toggleUserStatus('${user.username}', false)">🚫 Disable</button>`
: `<button class="btn btn-success" onclick="toggleUserStatus('${user.username}', true)">✅ Enable</button>`;
html += `
<tr>
<td><strong>${user.username}</strong></td>
<td>${roleBadge}</td>
<td>${user.created || 'N/A'}</td>
<td>${statusBadge}</td>
<td>
${toggleButton}
${deleteButton}
</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
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 = '<strong>❌ Error:</strong> Username and password are required';
return;
}
if (password.length < 6) {
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = '<strong>❌ Error:</strong> Password must be at least 6 characters';
return;
}
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-info';
resultDiv.innerHTML = '<strong>⏳</strong> 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 = `<strong>✅ Success:</strong> ${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 = `<strong>❌ Error:</strong> ${data.error}`;
}
} catch (error) {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${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 = '<strong>❌ Error:</strong> All fields are required';
return;
}
if (newPassword !== confirmPassword) {
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = '<strong>❌ Error:</strong> New passwords do not match';
return;
}
if (newPassword.length < 6) {
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = '<strong>❌ Error:</strong> Password must be at least 6 characters';
return;
}
resultDiv.style.display = 'block';
resultDiv.className = 'alert alert-info';
resultDiv.innerHTML = '<strong>⏳</strong> 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 = `<strong>✅ Success:</strong> ${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 = `<strong>❌ Error:</strong> ${data.error}`;
}
} catch (error) {
resultDiv.className = 'alert alert-danger';
resultDiv.innerHTML = `<strong>❌ Error:</strong> ${error.message}`;
}
}