diff --git a/dist/adastra-vtl-installer-1.0.0.tar.gz b/dist/adastra-vtl-installer-1.0.0.tar.gz index cea81f2..93ccbe8 100644 Binary files a/dist/adastra-vtl-installer-1.0.0.tar.gz and b/dist/adastra-vtl-installer-1.0.0.tar.gz differ diff --git a/dist/adastra-vtl-installer/VERSION b/dist/adastra-vtl-installer/VERSION index f086f9a..3ce113b 100644 --- a/dist/adastra-vtl-installer/VERSION +++ b/dist/adastra-vtl-installer/VERSION @@ -1,4 +1,4 @@ Adastra VTL Installer Version: 1.0.0 -Build Date: 2025-12-10 12:25:11 +Build Date: 2025-12-10 14:36:24 Build Host: vtl-dev diff --git a/dist/adastra-vtl-installer/web-ui/api.php b/dist/adastra-vtl-installer/web-ui/api.php index 70908cb..5a6a2cb 100644 --- a/dist/adastra-vtl-installer/web-ui/api.php +++ b/dist/adastra-vtl-installer/web-ui/api.php @@ -148,10 +148,16 @@ switch ($action) { getDeviceMapping(); break; + case 'library_status': + getLibraryStatus(); + break; + case 'system_health': getSystemHealth(); break; + + case 'restart_appliance': restartAppliance(); break; @@ -938,4 +944,59 @@ function getDeviceMapping() { 'raw_output' => $output ]); } + +function getLibraryStatus() { + // Find library contents file + $files = glob('/etc/mhvtl/library_contents.*'); + if (empty($files)) { + echo json_encode(['success' => false, 'error' => 'No library config found']); + return; + } + + // Use the first one found, typically library_contents.10 + $file = $files[0]; + $libId = substr(strrchr($file, '.'), 1); + + $lines = file($file); + $status = [ + 'library_id' => $libId, + 'drives' => [], + 'slots' => [], + 'maps' => [], + 'pickers' => [] + ]; + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line) || $line[0] == '#') continue; + + if (preg_match('/^Drive\s+(\d+):\s*(.*)$/i', $line, $matches)) { + $status['drives'][] = [ + 'id' => intval($matches[1]), + 'barcode' => trim($matches[2]), + 'full' => !empty(trim($matches[2])) + ]; + } elseif (preg_match('/^Slot\s+(\d+):\s*(.*)$/i', $line, $matches)) { + $status['slots'][] = [ + 'id' => intval($matches[1]), + 'barcode' => trim($matches[2]), + 'full' => !empty(trim($matches[2])) + ]; + } elseif (preg_match('/^MAP\s+(\d+):\s*(.*)$/i', $line, $matches)) { + $status['maps'][] = [ + 'id' => intval($matches[1]), + 'barcode' => trim($matches[2]), + 'full' => !empty(trim($matches[2])) + ]; + } elseif (preg_match('/^Picker\s+(\d+):\s*(.*)$/i', $line, $matches)) { + $status['pickers'][] = [ + 'id' => intval($matches[1]), + 'barcode' => trim($matches[2]), + 'full' => !empty(trim($matches[2])) + ]; + } + } + + echo json_encode(['success' => true, 'data' => $status]); +} ?> diff --git a/dist/adastra-vtl-installer/web-ui/index.html b/dist/adastra-vtl-installer/web-ui/index.html index 2dc4cc8..81ea183 100644 --- a/dist/adastra-vtl-installer/web-ui/index.html +++ b/dist/adastra-vtl-installer/web-ui/index.html @@ -208,6 +208,35 @@

Complete CRUD management for virtual tape files

+
+
+

👀 Library Visualizer

+ +
+
+ + + +
+
+

➕ Create New Tapes

diff --git a/dist/adastra-vtl-installer/web-ui/script.js b/dist/adastra-vtl-installer/web-ui/script.js index 6ef4a07..814ba03 100644 --- a/dist/adastra-vtl-installer/web-ui/script.js +++ b/dist/adastra-vtl-installer/web-ui/script.js @@ -87,6 +87,11 @@ function initNavigation() { this.classList.add('active'); document.getElementById(targetId).classList.add('active'); + + // Auto reload for Viz + if (targetId === 'manage-tapes') { + loadLibraryStatus(); + } }); }); } @@ -1941,3 +1946,63 @@ async function changeUserPassword() { resultDiv.innerHTML = `❌ Error: ${error.message}`; } } + +// Library Visualizer +function loadLibraryStatus() { + const loading = document.getElementById('viz-loading'); + const container = document.getElementById('library-viz'); + const errorEl = document.getElementById('viz-error'); + + if (!loading || !container || !errorEl) return; + + loading.style.display = 'block'; + container.style.display = 'none'; + errorEl.style.display = 'none'; + + fetch('api.php?action=library_status') + .then(response => response.json()) + .then(data => { + loading.style.display = 'none'; + if (data.success) { + container.style.display = 'flex'; + renderVizGrid('viz-drives', data.data.drives, 'drive'); + renderVizGrid('viz-maps', data.data.maps, 'map'); + renderVizGrid('viz-slots', data.data.slots, 'slot'); + } else { + errorEl.textContent = 'Error: ' + data.error; + errorEl.style.display = 'block'; + } + }) + .catch(err => { + loading.style.display = 'none'; + errorEl.textContent = 'Fetch Error: ' + err.message; + errorEl.style.display = 'block'; + }); +} + +function renderVizGrid(containerId, items, type) { + const grid = document.getElementById(containerId); + if (!grid) return; + grid.innerHTML = ''; + + if (!items || items.length === 0) { + grid.innerHTML = '
No items
'; + return; + } + + items.forEach(item => { + const el = document.createElement('div'); + el.className = `viz-slot ${item.full ? 'full' : 'empty'} ${type === 'drive' ? 'drive-slot' : ''}`; + + const icon = type === 'drive' ? '💾' : (type === 'map' ? '📥' : '📼'); + const label = item.full ? item.barcode : 'Empty'; + + el.innerHTML = ` + ${icon} +
${type.toUpperCase()} ${item.id}
+
${label}
+ `; + + grid.appendChild(el); + }); +} diff --git a/dist/adastra-vtl-installer/web-ui/style.css b/dist/adastra-vtl-installer/web-ui/style.css index aab9d95..e11fd54 100644 --- a/dist/adastra-vtl-installer/web-ui/style.css +++ b/dist/adastra-vtl-installer/web-ui/style.css @@ -440,3 +440,93 @@ main.container { font-size: 0.875rem; } } + +/* Library Visualizer Styles */ +.library-viz { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.viz-section h4 { + margin-bottom: 0.75rem; + color: var(--text-secondary); + text-transform: uppercase; + font-size: 0.875rem; + letter-spacing: 0.05em; +} + +.viz-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 0.75rem; +} + +.viz-slot { + background: var(--dark-bg); + border: 1px solid var(--border-color); + border-radius: 0.5rem; + padding: 0.75rem 0.5rem; + text-align: center; + transition: all 0.2s; + cursor: default; + position: relative; + box-shadow: 0 1px 2px rgba(0,0,0,0.1); +} + +.viz-slot:hover { + transform: translateY(-2px); + box-shadow: 0 4px 6px rgba(0,0,0,0.2); +} + +.viz-slot.full { + background: rgba(16, 185, 129, 0.1); + border-color: var(--success-color); +} + +.viz-slot .slot-icon { + font-size: 1.5rem; + margin-bottom: 0.25rem; + display: block; +} + +.viz-slot .slot-id { + font-size: 0.75rem; + color: var(--text-secondary); + margin-bottom: 0.25rem; + font-weight: 500; +} + +.viz-slot .tape-label { + font-size: 0.75rem; + font-weight: 600; + font-family: 'Courier New', monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text-secondary); +} + +.viz-slot.full .tape-label { + color: var(--success-color); +} + +.viz-slot.empty .tape-label { + color: var(--text-secondary); + font-style: italic; + opacity: 0.5; +} + +/* Drive specific styling */ +.drive-slot { + border-color: var(--info-color); +} + +.drive-slot.full { + background: rgba(59, 130, 246, 0.1); + border-color: var(--info-color); +} + +.drive-slot.full .tape-label { + color: var(--info-color); +} diff --git a/web-ui/api.php b/web-ui/api.php index 70908cb..5a6a2cb 100644 --- a/web-ui/api.php +++ b/web-ui/api.php @@ -148,10 +148,16 @@ switch ($action) { getDeviceMapping(); break; + case 'library_status': + getLibraryStatus(); + break; + case 'system_health': getSystemHealth(); break; + + case 'restart_appliance': restartAppliance(); break; @@ -938,4 +944,59 @@ function getDeviceMapping() { 'raw_output' => $output ]); } + +function getLibraryStatus() { + // Find library contents file + $files = glob('/etc/mhvtl/library_contents.*'); + if (empty($files)) { + echo json_encode(['success' => false, 'error' => 'No library config found']); + return; + } + + // Use the first one found, typically library_contents.10 + $file = $files[0]; + $libId = substr(strrchr($file, '.'), 1); + + $lines = file($file); + $status = [ + 'library_id' => $libId, + 'drives' => [], + 'slots' => [], + 'maps' => [], + 'pickers' => [] + ]; + + foreach ($lines as $line) { + $line = trim($line); + if (empty($line) || $line[0] == '#') continue; + + if (preg_match('/^Drive\s+(\d+):\s*(.*)$/i', $line, $matches)) { + $status['drives'][] = [ + 'id' => intval($matches[1]), + 'barcode' => trim($matches[2]), + 'full' => !empty(trim($matches[2])) + ]; + } elseif (preg_match('/^Slot\s+(\d+):\s*(.*)$/i', $line, $matches)) { + $status['slots'][] = [ + 'id' => intval($matches[1]), + 'barcode' => trim($matches[2]), + 'full' => !empty(trim($matches[2])) + ]; + } elseif (preg_match('/^MAP\s+(\d+):\s*(.*)$/i', $line, $matches)) { + $status['maps'][] = [ + 'id' => intval($matches[1]), + 'barcode' => trim($matches[2]), + 'full' => !empty(trim($matches[2])) + ]; + } elseif (preg_match('/^Picker\s+(\d+):\s*(.*)$/i', $line, $matches)) { + $status['pickers'][] = [ + 'id' => intval($matches[1]), + 'barcode' => trim($matches[2]), + 'full' => !empty(trim($matches[2])) + ]; + } + } + + echo json_encode(['success' => true, 'data' => $status]); +} ?> diff --git a/web-ui/index.html b/web-ui/index.html index 2dc4cc8..81ea183 100644 --- a/web-ui/index.html +++ b/web-ui/index.html @@ -208,6 +208,35 @@

Complete CRUD management for virtual tape files

+
+
+

👀 Library Visualizer

+ +
+
+ + + +
+
+

➕ Create New Tapes

diff --git a/web-ui/script.js b/web-ui/script.js index 6ef4a07..814ba03 100644 --- a/web-ui/script.js +++ b/web-ui/script.js @@ -87,6 +87,11 @@ function initNavigation() { this.classList.add('active'); document.getElementById(targetId).classList.add('active'); + + // Auto reload for Viz + if (targetId === 'manage-tapes') { + loadLibraryStatus(); + } }); }); } @@ -1941,3 +1946,63 @@ async function changeUserPassword() { resultDiv.innerHTML = `❌ Error: ${error.message}`; } } + +// Library Visualizer +function loadLibraryStatus() { + const loading = document.getElementById('viz-loading'); + const container = document.getElementById('library-viz'); + const errorEl = document.getElementById('viz-error'); + + if (!loading || !container || !errorEl) return; + + loading.style.display = 'block'; + container.style.display = 'none'; + errorEl.style.display = 'none'; + + fetch('api.php?action=library_status') + .then(response => response.json()) + .then(data => { + loading.style.display = 'none'; + if (data.success) { + container.style.display = 'flex'; + renderVizGrid('viz-drives', data.data.drives, 'drive'); + renderVizGrid('viz-maps', data.data.maps, 'map'); + renderVizGrid('viz-slots', data.data.slots, 'slot'); + } else { + errorEl.textContent = 'Error: ' + data.error; + errorEl.style.display = 'block'; + } + }) + .catch(err => { + loading.style.display = 'none'; + errorEl.textContent = 'Fetch Error: ' + err.message; + errorEl.style.display = 'block'; + }); +} + +function renderVizGrid(containerId, items, type) { + const grid = document.getElementById(containerId); + if (!grid) return; + grid.innerHTML = ''; + + if (!items || items.length === 0) { + grid.innerHTML = '
No items
'; + return; + } + + items.forEach(item => { + const el = document.createElement('div'); + el.className = `viz-slot ${item.full ? 'full' : 'empty'} ${type === 'drive' ? 'drive-slot' : ''}`; + + const icon = type === 'drive' ? '💾' : (type === 'map' ? '📥' : '📼'); + const label = item.full ? item.barcode : 'Empty'; + + el.innerHTML = ` + ${icon} +
${type.toUpperCase()} ${item.id}
+
${label}
+ `; + + grid.appendChild(el); + }); +} diff --git a/web-ui/style.css b/web-ui/style.css index aab9d95..e11fd54 100644 --- a/web-ui/style.css +++ b/web-ui/style.css @@ -440,3 +440,93 @@ main.container { font-size: 0.875rem; } } + +/* Library Visualizer Styles */ +.library-viz { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.viz-section h4 { + margin-bottom: 0.75rem; + color: var(--text-secondary); + text-transform: uppercase; + font-size: 0.875rem; + letter-spacing: 0.05em; +} + +.viz-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 0.75rem; +} + +.viz-slot { + background: var(--dark-bg); + border: 1px solid var(--border-color); + border-radius: 0.5rem; + padding: 0.75rem 0.5rem; + text-align: center; + transition: all 0.2s; + cursor: default; + position: relative; + box-shadow: 0 1px 2px rgba(0,0,0,0.1); +} + +.viz-slot:hover { + transform: translateY(-2px); + box-shadow: 0 4px 6px rgba(0,0,0,0.2); +} + +.viz-slot.full { + background: rgba(16, 185, 129, 0.1); + border-color: var(--success-color); +} + +.viz-slot .slot-icon { + font-size: 1.5rem; + margin-bottom: 0.25rem; + display: block; +} + +.viz-slot .slot-id { + font-size: 0.75rem; + color: var(--text-secondary); + margin-bottom: 0.25rem; + font-weight: 500; +} + +.viz-slot .tape-label { + font-size: 0.75rem; + font-weight: 600; + font-family: 'Courier New', monospace; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--text-secondary); +} + +.viz-slot.full .tape-label { + color: var(--success-color); +} + +.viz-slot.empty .tape-label { + color: var(--text-secondary); + font-style: italic; + opacity: 0.5; +} + +/* Drive specific styling */ +.drive-slot { + border-color: var(--info-color); +} + +.drive-slot.full { + background: rgba(59, 130, 246, 0.1); + border-color: var(--info-color); +} + +.drive-slot.full .tape-label { + color: var(--info-color); +}