2047 lines
73 KiB
JavaScript
2047 lines
73 KiB
JavaScript
/**
|
||
* 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 = `
|
||
<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('');
|
||
|
||
// Update Active Sessions Table
|
||
const sessionTbody = document.getElementById('iscsi-sessions-body');
|
||
const noSessionsMsg = document.getElementById('no-sessions-msg');
|
||
let hasSessions = false;
|
||
|
||
if (sessionTbody) {
|
||
let sessionsHtml = '';
|
||
data.targets.forEach(target => {
|
||
if (target.sessions && target.sessions.length > 0) {
|
||
target.sessions.forEach(session => {
|
||
hasSessions = true;
|
||
sessionsHtml += `
|
||
<tr>
|
||
<td>
|
||
<strong>TID ${target.tid}</strong><br>
|
||
<small style="font-size:0.75rem; color:#888;">${target.name}</small>
|
||
</td>
|
||
<td><code>${session.initiator}</code></td>
|
||
<td><span class="badge badge-success">${session.ip}</span></td>
|
||
</tr>
|
||
`;
|
||
});
|
||
}
|
||
});
|
||
|
||
sessionTbody.innerHTML = sessionsHtml;
|
||
|
||
if (noSessionsMsg) {
|
||
noSessionsMsg.style.display = hasSessions ? 'none' : 'block';
|
||
}
|
||
}
|
||
}
|
||
} 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}`;
|
||
}
|
||
}
|
||
|
||
// 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 = '<div style="color:#aaa; font-style:italic; grid-column: 1/-1;">No items</div>';
|
||
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 = `
|
||
<span class="slot-icon">${icon}</span>
|
||
<div class="slot-id">${type.toUpperCase()} ${item.id}</div>
|
||
<div class="tape-label" title="${label}">${label}</div>
|
||
`;
|
||
|
||
grid.appendChild(el);
|
||
});
|
||
}
|