The sql method described on the second page still works.
The CSV format is not convenient for viewing, so I made an offline web page with tabular and hierarchical output that can be printed and attached to tape.
Maybe it will be useful to someone.
Code: Select all
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<title>Veeam Tape Inventory Explorer | Tape Backup Hierarchy</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
background: #f0f4f8;
color: #1a2c3e;
padding: 2rem 1.5rem;
}
.container {
max-width: 1600px;
margin: 0 auto;
}
.header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 1.5rem;
gap: 1rem;
}
h1 {
font-size: 1.9rem;
font-weight: 600;
background: linear-gradient(135deg, #0b5e7a, #1c7e9c);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
letter-spacing: -0.3px;
}
.sub {
font-size: 0.85rem;
color: #4a627a;
margin-top: 0.3rem;
}
.file-loader {
background: white;
border-radius: 24px;
padding: 1.2rem 1.8rem;
margin-bottom: 1.8rem;
border: 1px solid #dce5ec;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.file-info {
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
font-size: 0.9rem;
}
.file-badge {
background: #eef2f6;
padding: 0.4rem 1rem;
border-radius: 40px;
font-family: monospace;
font-size: 0.8rem;
color: #1f6e8c;
}
.load-btn {
background: #2c7da0;
color: white;
border: none;
padding: 0.6rem 1.4rem;
border-radius: 40px;
font-weight: 500;
font-size: 0.85rem;
cursor: pointer;
transition: 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.load-btn:hover {
background: #1f5e7a;
transform: scale(1.01);
}
.status-msg {
font-size: 0.8rem;
color: #54809b;
}
.stats-bar {
background: white;
border-radius: 24px;
padding: 0.7rem 1.5rem;
box-shadow: 0 2px 6px rgba(0,0,0,0.02);
display: flex;
flex-wrap: wrap;
gap: 1.8rem;
margin-bottom: 1.8rem;
align-items: center;
font-weight: 500;
border: 1px solid #e2edf2;
}
.stat-item {
display: flex;
align-items: baseline;
gap: 0.4rem;
font-size: 0.9rem;
color: #1f4e6e;
}
.stat-value {
font-weight: 800;
font-size: 1.3rem;
color: #0f4c5f;
}
.controls {
background: white;
border-radius: 20px;
padding: 1rem 1.5rem;
margin-bottom: 1.8rem;
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
justify-content: space-between;
border: 1px solid #e2edf2;
}
.search-box {
flex: 2;
min-width: 220px;
display: flex;
align-items: center;
background: #f8fafd;
border-radius: 40px;
padding: 0.4rem 1rem;
border: 1px solid #cfdfe8;
}
.search-box i {
color: #7c9eb3;
margin-right: 8px;
}
.search-box input {
border: none;
background: transparent;
width: 100%;
font-size: 0.9rem;
padding: 0.5rem 0;
outline: none;
font-family: inherit;
}
.filter-group {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
align-items: center;
}
.filter-select {
background: #f8fafd;
border: 1px solid #cfdfe8;
border-radius: 30px;
padding: 0.5rem 1rem;
font-size: 0.85rem;
font-family: inherit;
cursor: pointer;
outline: none;
}
.reset-btn, .print-btn, .view-toggle-btn {
background: #eef3f7;
border: none;
padding: 0.5rem 1.1rem;
border-radius: 32px;
font-weight: 500;
font-size: 0.8rem;
cursor: pointer;
transition: 0.2s;
color: #1f5e7e;
display: inline-flex;
align-items: center;
gap: 6px;
}
.reset-btn:hover, .print-btn:hover, .view-toggle-btn:hover {
background: #e0eaf1;
}
.print-btn {
background: #2c7da0;
color: white;
}
.print-btn:hover {
background: #1f5e7a;
}
.view-toggle-btn.active {
background: #2c7da0;
color: white;
}
.table-wrapper {
background: white;
border-radius: 20px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
overflow-x: auto;
border: 1px solid #e6edf2;
}
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
min-width: 1400px;
}
.data-table th {
background: #eef3fa;
padding: 1rem 0.8rem;
text-align: left;
font-weight: 600;
color: #1f4662;
border-bottom: 1px solid #d4e2ec;
position: sticky;
top: 0;
background-color: #eef3fa;
white-space: nowrap;
}
.data-table td {
padding: 0.8rem;
border-bottom: 1px solid #ecf3f8;
color: #1e3b4f;
vertical-align: top;
word-break: break-word;
}
.data-table tr:hover td {
background-color: #f9fdfe;
}
.tree-view {
padding: 1rem;
}
.tree-node {
margin-left: 1.5rem;
list-style: none;
font-size: 0.85rem;
}
.tree-node-root {
margin-left: 0;
}
.tree-item {
padding: 0.4rem 0;
display: flex;
align-items: flex-start;
gap: 8px;
flex-wrap: wrap;
border-bottom: 1px solid #f0f2f5;
}
.tree-toggle {
cursor: pointer;
user-select: none;
width: 20px;
display: inline-flex;
justify-content: center;
font-weight: bold;
color: #2c7da0;
}
.tree-label {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: baseline;
flex: 1;
}
.tree-label-icon {
width: 24px;
color: #5f8a9f;
}
.tree-details {
display: flex;
flex-wrap: wrap;
gap: 16px;
font-size: 0.75rem;
color: #4a6f88;
}
.tree-details span {
background: #f0f4f9;
padding: 0.1rem 0.5rem;
border-radius: 20px;
}
.children-container {
padding-left: 1.8rem;
}
.badge {
display: inline-block;
padding: 0.2rem 0.6rem;
border-radius: 40px;
font-size: 0.7rem;
font-weight: 600;
text-align: center;
}
.badge-protected {
background: #dff0e6;
color: #1c6e43;
}
.badge-unprotected {
background: #fee9e6;
color: #bc4e2c;
}
.numeric {
font-family: monospace;
font-weight: 500;
}
.empty-row td {
text-align: center;
padding: 3rem;
color: #7e9ab0;
font-style: italic;
}
footer {
margin-top: 1.5rem;
text-align: center;
font-size: 0.75rem;
color: #6f8eaa;
}
.print-header {
display: none;
text-align: center;
margin-bottom: 1rem;
border-bottom: 2px solid #2c7da0;
padding-bottom: 0.5rem;
}
.print-header h2 {
font-size: 1.2rem;
font-weight: 600;
color: #1a2c3e;
}
.print-header p {
font-size: 0.8rem;
color: #4a627a;
margin-top: 0.2rem;
}
@media print {
body {
background: white;
padding: 0;
margin: 0;
font-size: 8pt !important;
}
.file-loader, .controls, .stats-bar, footer, .print-btn, .reset-btn, .load-btn, .view-toggle-btn {
display: none !important;
}
#tableView, #treeView {
display: none !important;
}
body.printing-table #tableView {
display: block !important;
}
body.printing-tree #treeView {
display: block !important;
}
.print-header {
display: block !important;
}
.table-wrapper {
box-shadow: none;
border: none;
overflow-x: visible !important;
width: 100%;
margin: 0;
padding: 0;
background: white;
}
.data-table {
min-width: 100% !important;
width: auto !important;
font-size: 6.5pt !important;
border-collapse: collapse;
page-break-inside: avoid;
}
.data-table th, .data-table td {
border: 1px solid #aaa !important;
padding: 0.2rem 0.3rem !important;
white-space: normal !important;
word-break: break-word;
vertical-align: top;
}
.data-table th {
background-color: #eef3fa !important;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
font-size: 6.5pt !important;
}
.badge {
font-size: 5.5pt !important;
padding: 0.1rem 0.3rem !important;
white-space: nowrap;
}
.data-table tr {
page-break-inside: avoid;
break-inside: avoid;
}
.tree-view {
display: block !important;
overflow-x: visible;
font-size: 7pt !important;
}
.tree-item, .tree-details {
break-inside: avoid;
}
.tree-details span {
font-size: 6pt !important;
padding: 0.1rem 0.3rem;
}
}
@media (max-width: 700px) {
body {
padding: 1rem;
}
.controls {
flex-direction: column;
align-items: stretch;
}
.file-loader {
flex-direction: column;
align-items: stretch;
}
}
@page {
size: landscape;
margin: 1rem;
}
</style>
</head>
<body>
<div class="container">
<div class="print-header" id="printHeader">
<h2><i class="fas fa-tape"></i> Veeam Tape Inventory Explorer</h2>
<p id="printFileName">File: <span>not loaded</span></p>
<p id="printTimestamp">Print date: <span></span></p>
</div>
<div class="header">
<div>
<h1><i class="fas fa-tape" style="color: #2c7da0;"></i> Veeam Tape Inventory Explorer</h1>
<div class="sub">Tape backup management โ table / folder & file hierarchy</div>
</div>
</div>
<div class="file-loader">
<div class="file-info">
<i class="fas fa-file-csv" style="font-size: 1.4rem; color: #2c7da0;"></i>
<span><strong>Data source:</strong> </span>
<span id="filePathDisplay" class="file-badge">No file selected</span>
<span id="loadStatus" class="status-msg"><i class="fas fa-info-circle"></i> Click "Choose File" to load CSV</span>
</div>
<button id="chooseFileBtn" class="load-btn"><i class="fas fa-folder-open"></i> Choose File</button>
<input type="file" id="fileInput" accept=".csv" style="display: none;" />
</div>
<div class="stats-bar" id="statsBar">
<div class="stat-item"><span class="stat-value" id="totalRows">0</span> <span>records</span></div>
<div class="stat-item"><i class="fas fa-hdd"></i> Total size: <span class="stat-value" id="totalSizeGB">0</span> GB</div>
<div class="stat-item"><i class="fas fa-shield-alt"></i> Protected: <span class="stat-value" id="protectedCount">0</span></div>
<div class="stat-item"><i class="fas fa-database"></i> Unique tapes: <span class="stat-value" id="uniqueTapes">0</span></div>
</div>
<div class="controls" id="controlsPanel" style="display: none;">
<div class="search-box">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" placeholder="Search all fields (file name, server, barcode, pool...)">
</div>
<div class="filter-group">
<select id="filterProtected" class="filter-select">
<option value="all">All media</option>
<option value="true">Protected (true)</option>
<option value="false">Unprotected (false)</option>
</select>
<select id="filterMediaPool" class="filter-select">
<option value="all">All media pools</option>
</select>
<button id="resetFiltersBtn" class="reset-btn"><i class="fas fa-undo-alt"></i> Reset</button>
<button id="toggleViewBtn" class="view-toggle-btn"><i class="fas fa-folder-tree"></i> Hierarchy</button>
<button id="printPdfBtn" class="print-btn"><i class="fas fa-print"></i> Print / PDF</button>
</div>
</div>
<div class="table-wrapper" id="tableWrapper">
<div id="tableView" style="display: block;">
<table class="data-table" id="dataTable">
<thead>
<tr>
<th>BarcodeID</th><th>Backup Server</th><th>Folder Path</th><th>File Name</th><th>Segment #</th><th>Size (GB)</th>
<th>Tape Cap.(GB)</th><th>Tape Rem.(GB)</th><th>Protected</th><th>Tape Location</th><th>Backup Job</th>
<th>Backup Set</th><th>Set Expiration</th><th>Last Write</th><th>Tape Descr.</th><th>Media Pool</th><th>Pool Description</th>
</tr>
</thead>
<tbody id="tableBody">
<tr class="empty-row"><td colspan="17">๐ฟ Waiting for CSV file upload. Click "Choose File" and select Veeam report. ๐</td></tr>
</tbody>
</table>
</div>
<div id="treeView" style="display: none;" class="tree-view"></div>
</div>
<footer>
<i class="fas fa-info-circle"></i> Supported columns: BarcodeID, Backup_Server, Folder_Path, File_Name, FileSegmentNumber, File_Size_GB, Tape_Capacity_GB, Tape_Remaining_GB, IsTapeProtected, Tape_Physical_Location, Tape_Backup_Job, Tape_Backup_Set, Tape_Backup_Set_Expiration, Last_Write_Time, Tape_Description, Tape_Media_Pool, Tape_Media_Pool_Description.
</footer>
</div>
<script>
let dataset = [];
let currentView = 'table';
let currentFileName = "No file selected";
const tbody = document.getElementById('tableBody');
const treeContainer = document.getElementById('treeView');
const tableViewDiv = document.getElementById('tableView');
const treeViewDiv = document.getElementById('treeView');
const searchInput = document.getElementById('searchInput');
const filterProtected = document.getElementById('filterProtected');
const filterMediaPool = document.getElementById('filterMediaPool');
const resetBtn = document.getElementById('resetFiltersBtn');
const printBtn = document.getElementById('printPdfBtn');
const toggleViewBtn = document.getElementById('toggleViewBtn');
const totalRowsSpan = document.getElementById('totalRows');
const totalSizeSpan = document.getElementById('totalSizeGB');
const protectedCountSpan = document.getElementById('protectedCount');
const uniqueTapesSpan = document.getElementById('uniqueTapes');
const controlsPanel = document.getElementById('controlsPanel');
const loadStatusSpan = document.getElementById('loadStatus');
const filePathDisplaySpan = document.getElementById('filePathDisplay');
const chooseFileBtn = document.getElementById('chooseFileBtn');
const fileInput = document.getElementById('fileInput');
const printFileNameSpan = document.getElementById('printFileName');
const printTimestampSpan = document.getElementById('printTimestamp').querySelector('span');
function updatePrintHeader() {
if (printFileNameSpan) {
printFileNameSpan.innerHTML = `File: ${currentFileName}`;
}
if (printTimestampSpan) {
const now = new Date();
const formatted = now.toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' });
printTimestampSpan.textContent = formatted;
}
}
function parseCSVToObjects(csvText) {
const lines = csvText.split(/\r?\n/);
if (lines.length < 2) return [];
const firstLine = lines[0];
let headers = parseCSVLine(firstLine);
headers = headers.map(h => h.replace(/^\uFEFF/, '').trim());
const records = [];
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === '') continue;
const values = parseCSVLine(lines[i]);
if (values.length === 0) continue;
const obj = {};
headers.forEach((header, idx) => {
let val = values[idx] !== undefined ? values[idx] : '';
if (header === 'FileSegmentNumber') {
let num = parseInt(val, 10);
obj[header] = isNaN(num) ? 0 : num;
} else if (header === 'File_Size_GB' || header === 'Tape_Capacity_GB' || header === 'Tape_Remaining_GB') {
let num = parseFloat(val);
obj[header] = isNaN(num) ? 0 : num;
} else if (header === 'IsTapeProtected') {
let lowerVal = val.toString().toLowerCase();
obj[header] = (lowerVal === 'true' || lowerVal === '1' || lowerVal === 'yes');
} else {
obj[header] = val;
}
});
records.push(obj);
}
return records;
}
function parseCSVLine(line) {
const result = [];
let inQuotes = false;
let current = '';
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (ch === '"') {
if (inQuotes && line[i+1] === '"') {
current += '"';
i++;
} else {
inQuotes = !inQuotes;
}
} else if (ch === ',' && !inQuotes) {
result.push(current.trim());
current = '';
} else {
current += ch;
}
}
result.push(current.trim());
return result;
}
function updateMediaPoolFilter(data) {
const pools = new Set();
data.forEach(record => {
const pool = record.Tape_Media_Pool;
if (pool && pool.toString().trim() !== '') pools.add(pool.toString());
});
let options = '<option value="all">All media pools</option>';
Array.from(pools).sort().forEach(p => {
options += `<option value="${escapeHtml(p)}">${escapeHtml(p)}</option>`;
});
filterMediaPool.innerHTML = options;
}
function filterData() {
const searchTerm = searchInput.value.trim().toLowerCase();
const protectedFilter = filterProtected.value;
const mediaPoolFilter = filterMediaPool.value;
return dataset.filter(record => {
if (protectedFilter !== 'all') {
const isProtected = record.IsTapeProtected === true;
if (protectedFilter === 'true' && !isProtected) return false;
if (protectedFilter === 'false' && isProtected) return false;
}
if (mediaPoolFilter !== 'all') {
const recordPool = record.Tape_Media_Pool ? record.Tape_Media_Pool.toString() : '';
if (recordPool !== mediaPoolFilter) return false;
}
if (searchTerm !== '') {
const searchableFields = [
record.BarcodeID, record.Backup_Server, record.Folder_Path, record.File_Name,
record.Tape_Physical_Location, record.Tape_Backup_Job, record.Tape_Backup_Set,
record.Tape_Backup_Set_Expiration, record.Last_Write_Time, record.Tape_Description,
record.Tape_Media_Pool, record.Tape_Media_Pool_Description,
(record.FileSegmentNumber?.toString() || ''), (record.File_Size_GB?.toString() || '')
];
return searchableFields.some(field => field && field.toString().toLowerCase().includes(searchTerm));
}
return true;
});
}
function escapeHtml(str) {
if (str === undefined || str === null) return '';
return String(str).replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
function formatNumber(val, decimals = 1) {
if (typeof val !== 'number' || isNaN(val)) return '0';
return val.toFixed(decimals);
}
function formatBoolean(value) {
if (value === true) return '<span class="badge badge-protected"><i class="fas fa-lock"></i> Protected</span>';
return '<span class="badge badge-unprotected"><i class="fas fa-unlock-alt"></i> No</span>';
}
function truncate(str, maxLen) {
if (!str) return '';
if (str.length <= maxLen) return str;
return str.substring(0, maxLen) + 'โฆ';
}
function renderTable(records) {
if (!records.length) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="17">๐ญ No data matching filters.</td></tr>';
return;
}
let html = '';
for (let rec of records) {
html += `<tr>
<td>${escapeHtml(rec.BarcodeID)}</td>
<td>${escapeHtml(rec.Backup_Server)}</td>
<td title="${escapeHtml(rec.Folder_Path)}">${escapeHtml(truncate(rec.Folder_Path, 60))}</td>
<td>${escapeHtml(rec.File_Name)}</td>
<td class="numeric">${rec.FileSegmentNumber || 0}</td>
<td class="numeric">${formatNumber(rec.File_Size_GB, 1)}</td>
<td class="numeric">${formatNumber(rec.Tape_Capacity_GB, 0)}</td>
<td class="numeric">${formatNumber(rec.Tape_Remaining_GB, 1)}</td>
<td>${formatBoolean(rec.IsTapeProtected)}</td>
<td>${escapeHtml(rec.Tape_Physical_Location)}</td>
<td>${escapeHtml(rec.Tape_Backup_Job)}</td>
<td>${escapeHtml(rec.Tape_Backup_Set)}</td>
<td>${escapeHtml(rec.Tape_Backup_Set_Expiration)}</td>
<td>${escapeHtml(rec.Last_Write_Time)}</td>
<td>${escapeHtml(rec.Tape_Description)}</td>
<td>${escapeHtml(rec.Tape_Media_Pool)}</td>
<td>${escapeHtml(rec.Tape_Media_Pool_Description)}</td>
</tr>`;
}
tbody.innerHTML = html;
}
function buildHierarchy(records) {
const tree = [];
const serverMap = new Map();
for (const rec of records) {
const server = rec.Backup_Server || 'Unknown Server';
if (!serverMap.has(server)) serverMap.set(server, []);
serverMap.get(server).push(rec);
}
for (const [server, serverRecords] of serverMap.entries()) {
const serverNode = { name: server, type: 'server', children: [], items: [] };
const pathMap = new Map();
for (const rec of serverRecords) {
let folderPath = rec.Folder_Path || '/';
if (!folderPath.startsWith('/')) folderPath = '/' + folderPath;
if (!pathMap.has(folderPath)) pathMap.set(folderPath, []);
pathMap.get(folderPath).push(rec);
}
for (const [path, pathRecords] of pathMap.entries()) {
const pathParts = path.split('/').filter(p => p);
let currentLevel = serverNode.children;
for (let i = 0; i < pathParts.length; i++) {
const part = pathParts[i];
let existing = currentLevel.find(n => n.name === part && n.type === 'folder');
if (!existing) {
existing = { name: part, type: 'folder', children: [], items: [], fullPath: pathParts.slice(0,i+1).join('/') };
currentLevel.push(existing);
}
if (i === pathParts.length - 1) {
for (const rec of pathRecords) existing.items.push(rec);
}
currentLevel = existing.children;
}
}
tree.push(serverNode);
}
return tree;
}
function renderTree(records) {
if (!records.length) {
treeContainer.innerHTML = '<div class="empty-row" style="padding:2rem;text-align:center;">๐ญ No data to display in hierarchy</div>';
return;
}
const hierarchy = buildHierarchy(records);
treeContainer.innerHTML = '<ul class="tree-node-root" style="list-style:none;padding-left:0;">' + renderTreeNodes(hierarchy) + '</ul>';
document.querySelectorAll('.tree-toggle').forEach(toggle => {
toggle.addEventListener('click', function(e) {
const targetId = this.getAttribute('data-target');
const childContainer = document.getElementById(targetId);
if (childContainer) {
const isVisible = childContainer.style.display !== 'none';
childContainer.style.display = isVisible ? 'none' : 'block';
this.textContent = isVisible ? 'โถ' : 'โผ';
}
e.stopPropagation();
});
});
}
function renderTreeNodes(nodes) {
let html = '';
for (const node of nodes) {
const nodeId = `tree_${Math.random().toString(36).substr(2, 8)}_${node.name.replace(/[^a-z0-9]/gi, '_')}`;
if (node.type === 'server') {
html += `<li class="tree-node">
<div class="tree-item">
<span class="tree-toggle" data-target="${nodeId}" style="cursor:pointer;">โผ</span>
<i class="fas fa-server tree-label-icon" style="color:#2c7da0;"></i>
<span class="tree-label"><strong>${escapeHtml(node.name)}</strong></span>
</div>
<div id="${nodeId}" class="children-container">
${node.children?.length ? '<ul class="tree-node" style="list-style:none;">' + renderTreeNodes(node.children) + '</ul>' : ''}
${node.items?.length ? renderFileItems(node.items) : ''}
</div>
</li>`;
} else if (node.type === 'folder') {
html += `<li class="tree-node">
<div class="tree-item">
<span class="tree-toggle" data-target="${nodeId}" style="cursor:pointer;">โผ</span>
<i class="fas fa-folder tree-label-icon" style="color:#e6a017;"></i>
<span class="tree-label">๐ ${escapeHtml(node.name)}</span>
</div>
<div id="${nodeId}" class="children-container">
${node.children?.length ? '<ul class="tree-node" style="list-style:none;">' + renderTreeNodes(node.children) + '</ul>' : ''}
${node.items?.length ? renderFileItems(node.items) : ''}
</div>
</li>`;
}
}
return html;
}
function renderFileItems(items) {
let html = '<ul class="tree-node" style="list-style:none;">';
for (const rec of items) {
html += `<li class="tree-node">
<div class="tree-item">
<span style="width:20px;display:inline-block;"></span>
<i class="fas fa-file tree-label-icon" style="color:#6c8ea0;"></i>
<span class="tree-label">
<strong>${escapeHtml(rec.File_Name)}</strong>
<div class="tree-details">
<span>๐ ${formatNumber(rec.File_Size_GB, 1)} GB</span>
<span>๐๏ธ Segment ${rec.FileSegmentNumber || 0}</span>
<span>๐ท๏ธ ${escapeHtml(rec.BarcodeID)}</span>
<span>${rec.IsTapeProtected ? '๐ Protected' : '๐ Unprotected'}</span>
<span>๐ Pool: ${escapeHtml(rec.Tape_Media_Pool || '-')}</span>
</div>
</span>
</div>
</li>`;
}
html += '</ul>';
return html;
}
function updateStats(records) {
const totalFiltered = records.length;
let totalSize = 0;
let protectedCnt = 0;
const tapeIDs = new Set();
for (let rec of records) {
totalSize += (rec.File_Size_GB || 0);
if (rec.IsTapeProtected === true) protectedCnt++;
if (rec.BarcodeID) tapeIDs.add(rec.BarcodeID);
}
totalRowsSpan.innerText = totalFiltered;
totalSizeSpan.innerText = formatNumber(totalSize, 1);
protectedCountSpan.innerText = protectedCnt;
uniqueTapesSpan.innerText = tapeIDs.size;
}
function refreshDisplay() {
if (!dataset.length) return;
const filtered = filterData();
updateStats(filtered);
if (currentView === 'table') {
tableViewDiv.style.display = 'block';
treeViewDiv.style.display = 'none';
renderTable(filtered);
toggleViewBtn.innerHTML = '<i class="fas fa-folder-tree"></i> Hierarchy';
} else {
tableViewDiv.style.display = 'none';
treeViewDiv.style.display = 'block';
renderTree(filtered);
toggleViewBtn.innerHTML = '<i class="fas fa-table"></i> Table';
}
}
function toggleView() {
currentView = currentView === 'table' ? 'tree' : 'table';
refreshDisplay();
}
function printCurrentView() {
if (!dataset.length) {
alert("No data to print. Please load a CSV file first.");
return;
}
updatePrintHeader();
if (currentView === 'table') {
document.body.classList.add('printing-table');
document.body.classList.remove('printing-tree');
} else {
document.body.classList.add('printing-tree');
document.body.classList.remove('printing-table');
}
window.print();
setTimeout(() => {
document.body.classList.remove('printing-table', 'printing-tree');
}, 100);
}
function loadFromCsvText(csvText, fileLabel) {
try {
const parsed = parseCSVToObjects(csvText);
if (!parsed.length) {
loadStatusSpan.innerHTML = '<i class="fas fa-exclamation-triangle"></i> File contains no valid data or headers do not match.';
return false;
}
dataset = parsed;
currentFileName = fileLabel;
updateMediaPoolFilter(dataset);
searchInput.value = '';
filterProtected.value = 'all';
filterMediaPool.value = 'all';
currentView = 'table';
refreshDisplay();
loadStatusSpan.innerHTML = `<i class="fas fa-check-circle"></i> Loaded ${dataset.length} records (${fileLabel})`;
filePathDisplaySpan.innerText = fileLabel;
controlsPanel.style.display = 'flex';
document.title = `Veeam Tape Inventory Explorer โ ${fileLabel}`;
updatePrintHeader();
return true;
} catch (err) {
loadStatusSpan.innerHTML = `<i class="fas fa-times-circle"></i> Error: ${err.message}`;
return false;
}
}
function loadFromFile(file) {
if (!file) return;
loadStatusSpan.innerHTML = '<i class="fas fa-spinner fa-pulse"></i> Reading file...';
const reader = new FileReader();
reader.onload = function(e) { loadFromCsvText(e.target.result, file.name); };
reader.onerror = function() { loadStatusSpan.innerHTML = '<i class="fas fa-times-circle"></i> File read error.'; };
reader.readAsText(file, 'UTF-8');
}
function init() {
chooseFileBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => {
if (e.target.files && e.target.files.length > 0) loadFromFile(e.target.files[0]);
fileInput.value = '';
});
resetBtn.addEventListener('click', () => {
if (dataset.length) {
searchInput.value = '';
filterProtected.value = 'all';
filterMediaPool.value = 'all';
refreshDisplay();
loadStatusSpan.innerHTML = `<i class="fas fa-eraser"></i> Filters reset, showing all ${dataset.length} records.`;
}
});
printBtn.addEventListener('click', printCurrentView);
toggleViewBtn.addEventListener('click', toggleView);
searchInput.addEventListener('input', refreshDisplay);
filterProtected.addEventListener('change', refreshDisplay);
filterMediaPool.addEventListener('change', refreshDisplay);
controlsPanel.style.display = 'none';
updatePrintHeader();
}
init();
</script>
</body>
</html>