Incorporate BCT branding: Add Logo, favicon, and update styles

This commit is contained in:
2025-12-01 00:11:43 +11:00
parent 35b7528d0a
commit 4bfa62dbf3
4 changed files with 1722 additions and 0 deletions

718
script.js Normal file
View File

@@ -0,0 +1,718 @@
// ===================================
// RAID Calculator - Core Logic
// ===================================
// Storage conversion constants
const TB_TO_BYTES_DECIMAL = 1000000000000; // 1 TB = 1,000,000,000,000 bytes (decimal)
const TB_TO_BYTES_BINARY = 1099511627776; // 1 TiB = 1,099,511,627,776 bytes (binary)
// RAID Configuration Metadata
const RAID_CONFIGS = {
raid0: {
name: 'RAID 0',
description: 'Striping - Maximum performance, no redundancy',
minDrives: 2,
faultTolerance: 0,
readPerformance: 'Excellent',
writePerformance: 'Excellent',
efficiency: 1.0
},
raid1: {
name: 'RAID 1',
description: 'Mirroring - 100% redundancy',
minDrives: 2,
faultTolerance: 1,
readPerformance: 'Good',
writePerformance: 'Moderate',
efficiency: 0.5
},
raid5: {
name: 'RAID 5',
description: 'Single parity - Good balance of performance and redundancy',
minDrives: 3,
faultTolerance: 1,
readPerformance: 'Good',
writePerformance: 'Moderate',
efficiency: null // Calculated: (n-1)/n
},
raid6: {
name: 'RAID 6',
description: 'Double parity - Enhanced fault tolerance',
minDrives: 4,
faultTolerance: 2,
readPerformance: 'Good',
writePerformance: 'Moderate',
efficiency: null // Calculated: (n-2)/n
},
raid10: {
name: 'RAID 10',
description: 'Mirrored stripes - Best performance with redundancy',
minDrives: 4,
faultTolerance: 1,
readPerformance: 'Excellent',
writePerformance: 'Good',
efficiency: 0.5
},
shr: {
name: 'Synology Hybrid RAID',
description: 'Flexible RAID with single disk fault tolerance',
minDrives: 2,
faultTolerance: 1,
readPerformance: 'Good',
writePerformance: 'Moderate',
efficiency: null // Calculated based on drive configuration
},
shr2: {
name: 'Synology Hybrid RAID 2',
description: 'Flexible RAID with dual disk fault tolerance',
minDrives: 4,
faultTolerance: 2,
readPerformance: 'Good',
writePerformance: 'Moderate',
efficiency: null // Calculated based on drive configuration
},
'zfs-stripe': {
name: 'ZFS Stripe',
description: 'No redundancy - Maximum capacity',
minDrives: 1,
faultTolerance: 0,
readPerformance: 'Excellent',
writePerformance: 'Excellent',
efficiency: 1.0
},
'zfs-mirror': {
name: 'ZFS Mirror',
description: 'Mirrored vdevs - High redundancy',
minDrives: 2,
faultTolerance: 1,
readPerformance: 'Excellent',
writePerformance: 'Good',
efficiency: 0.5
},
raidz1: {
name: 'RAIDZ1',
description: 'Single parity - ZFS equivalent to RAID 5',
minDrives: 3,
faultTolerance: 1,
readPerformance: 'Good',
writePerformance: 'Moderate',
efficiency: null // Calculated: (n-1)/n
},
raidz2: {
name: 'RAIDZ2',
description: 'Double parity - ZFS equivalent to RAID 6',
minDrives: 4,
faultTolerance: 2,
readPerformance: 'Good',
writePerformance: 'Moderate',
efficiency: null // Calculated: (n-2)/n
},
raidz3: {
name: 'RAIDZ3',
description: 'Triple parity - Maximum fault tolerance',
minDrives: 5,
faultTolerance: 3,
readPerformance: 'Good',
writePerformance: 'Moderate',
efficiency: null // Calculated: (n-3)/n
},
'unraid-1': {
name: 'Unraid (1 Parity)',
description: 'Flexible array with single parity drive - Individual drive access',
minDrives: 2,
faultTolerance: 1,
readPerformance: 'Good',
writePerformance: 'Moderate',
efficiency: null // Calculated: (n-1)/n
},
'unraid-2': {
name: 'Unraid (2 Parity)',
description: 'Flexible array with dual parity drives - Individual drive access',
minDrives: 3,
faultTolerance: 2,
readPerformance: 'Good',
writePerformance: 'Moderate',
efficiency: null // Calculated: (n-2)/n
}
};
// ===================================
// Storage Calculation Functions
// ===================================
/**
* Convert TB (decimal) to actual usable TiB (binary)
* Example: 12 TB = 10.91 TiB usable
*/
function convertTBtoTiB(tb) {
const bytes = tb * TB_TO_BYTES_DECIMAL;
const tib = bytes / TB_TO_BYTES_BINARY;
return tib;
}
/**
* Calculate usable storage for RAID configuration
*/
function calculateRAIDStorage(raidType, drives) {
const numDrives = drives.length;
const config = RAID_CONFIGS[raidType];
// Validate minimum drives
if (numDrives < config.minDrives) {
throw new Error(`${config.name} requires at least ${config.minDrives} drives`);
}
// Convert all drives to TiB (binary)
const drivesInTiB = drives.map(tb => convertTBtoTiB(tb));
let usableCapacity = 0;
let rawCapacity = drivesInTiB.reduce((sum, size) => sum + size, 0);
switch (raidType) {
case 'raid0':
case 'zfs-stripe':
// Sum of all drives
usableCapacity = rawCapacity;
break;
case 'raid1':
case 'zfs-mirror':
// Smallest drive capacity (all drives mirror the smallest)
usableCapacity = Math.min(...drivesInTiB);
break;
case 'raid5':
case 'raidz1':
// (n-1) * smallest drive
usableCapacity = (numDrives - 1) * Math.min(...drivesInTiB);
break;
case 'raid6':
case 'raidz2':
// (n-2) * smallest drive
usableCapacity = (numDrives - 2) * Math.min(...drivesInTiB);
break;
case 'raidz3':
// (n-3) * smallest drive
usableCapacity = (numDrives - 3) * Math.min(...drivesInTiB);
break;
case 'raid10':
// n/2 * smallest drive (assumes even number of drives)
if (numDrives % 2 !== 0) {
throw new Error('RAID 10 requires an even number of drives');
}
usableCapacity = (numDrives / 2) * Math.min(...drivesInTiB);
break;
case 'shr':
// Synology Hybrid RAID calculation
usableCapacity = calculateSHR(drivesInTiB, 1);
break;
case 'shr2':
// Synology Hybrid RAID 2 calculation
usableCapacity = calculateSHR(drivesInTiB, 2);
break;
case 'unraid-1':
// Unraid with 1 parity drive - sum of all drives minus largest (parity)
usableCapacity = calculateUnraid(drivesInTiB, 1);
break;
case 'unraid-2':
// Unraid with 2 parity drives - sum of all drives minus 2 largest (parity)
usableCapacity = calculateUnraid(drivesInTiB, 2);
break;
}
return {
usableCapacity,
rawCapacity,
efficiency: usableCapacity / rawCapacity,
wastedSpace: rawCapacity - usableCapacity
};
}
/**
* Calculate Synology Hybrid RAID capacity
* SHR optimizes storage by using different RAID levels based on drive sizes
*/
function calculateSHR(drivesInTiB, parityDisks) {
const sorted = [...drivesInTiB].sort((a, b) => a - b);
const numDrives = sorted.length;
if (numDrives < parityDisks + 1) {
throw new Error(`SHR-${parityDisks} requires at least ${parityDisks + 1} drives`);
}
let usableCapacity = 0;
// SHR algorithm: Build up capacity by size tiers
for (let i = 0; i < numDrives; i++) {
const currentSize = sorted[i];
const availableDrives = numDrives - i;
if (availableDrives > parityDisks) {
// Calculate contribution of this drive
if (i === 0) {
// First (smallest) drives contribute their full capacity minus parity
usableCapacity += currentSize * (availableDrives - parityDisks) / availableDrives;
} else {
// Larger drives contribute the difference from the previous tier
const difference = currentSize - sorted[i - 1];
usableCapacity += difference * (availableDrives - parityDisks) / availableDrives;
}
}
}
return usableCapacity;
}
/**
* Calculate Unraid capacity
* Unraid uses individual drives with parity - largest drive(s) become parity
* Usable capacity = sum of all drives minus the largest N drives (where N = parity count)
*/
function calculateUnraid(drivesInTiB, parityCount) {
const sorted = [...drivesInTiB].sort((a, b) => b - a); // Sort descending
const numDrives = sorted.length;
if (numDrives < parityCount + 1) {
throw new Error(`Unraid with ${parityCount} parity requires at least ${parityCount + 1} drives`);
}
// Remove the largest N drives (parity drives)
const dataDrives = sorted.slice(parityCount);
// Sum remaining drives for usable capacity
return dataDrives.reduce((sum, size) => sum + size, 0);
}
/**
* Calculate ZFS storage with multiple vdevs
* Each vdev uses the specified RAID type, and total capacity is the sum of all vdevs
*/
function calculateZFSVdevStorage(raidType, allDrives, numVdevs, drivesPerVdev) {
const totalDrives = allDrives.length;
if (totalDrives !== numVdevs * drivesPerVdev) {
throw new Error(`Total drives (${totalDrives}) must equal vdevs (${numVdevs}) × drives per vdev (${drivesPerVdev})`);
}
// Convert all drives to TiB
const drivesInTiB = allDrives.map(tb => convertTBtoTiB(tb));
let totalUsableCapacity = 0;
const rawCapacity = drivesInTiB.reduce((sum, size) => sum + size, 0);
// Calculate capacity for each vdev
for (let vdevIndex = 0; vdevIndex < numVdevs; vdevIndex++) {
const startIdx = vdevIndex * drivesPerVdev;
const endIdx = startIdx + drivesPerVdev;
const vdevDrives = drivesInTiB.slice(startIdx, endIdx);
const smallestInVdev = Math.min(...vdevDrives);
let vdevCapacity = 0;
switch (raidType) {
case 'zfs-mirror':
// Mirror: capacity of smallest drive in vdev
vdevCapacity = smallestInVdev;
break;
case 'raidz1':
// RAIDZ1: (n-1) * smallest drive
vdevCapacity = (drivesPerVdev - 1) * smallestInVdev;
break;
case 'raidz2':
// RAIDZ2: (n-2) * smallest drive
vdevCapacity = (drivesPerVdev - 2) * smallestInVdev;
break;
case 'raidz3':
// RAIDZ3: (n-3) * smallest drive
vdevCapacity = (drivesPerVdev - 3) * smallestInVdev;
break;
}
totalUsableCapacity += vdevCapacity;
}
return {
usableCapacity: totalUsableCapacity,
rawCapacity,
efficiency: totalUsableCapacity / rawCapacity,
wastedSpace: rawCapacity - totalUsableCapacity
};
}
/**
* Format capacity for display
*/
function formatCapacity(tib, showBoth = true) {
const tb = (tib * TB_TO_BYTES_BINARY) / TB_TO_BYTES_DECIMAL;
if (showBoth) {
return `${tib.toFixed(2)} TiB (${tb.toFixed(2)} TB)`;
}
return `${tib.toFixed(2)} TiB`;
}
/**
* Format percentage
*/
function formatPercentage(value) {
return `${(value * 100).toFixed(1)}%`;
}
// ===================================
// UI Interaction Functions
// ===================================
class RAIDCalculator {
constructor() {
this.initializeElements();
this.attachEventListeners();
this.updateDriveDisplay();
this.handleRaidTypeChange();
}
initializeElements() {
this.raidTypeSelect = document.getElementById('raid-type');
this.numDrivesSlider = document.getElementById('num-drives');
this.numDrivesDisplay = document.getElementById('num-drives-display');
this.driveSizeSelect = document.getElementById('drive-size');
this.mixedDrivesToggle = document.getElementById('mixed-drives');
this.customDrivesContainer = document.getElementById('custom-drives-container');
this.driveInputsContainer = document.getElementById('drive-inputs');
this.calculateBtn = document.getElementById('calculate-btn');
this.resultsContent = document.getElementById('results-content');
// ZFS vdev elements
this.zfsVdevConfig = document.getElementById('zfs-vdev-config');
this.numVdevsSlider = document.getElementById('num-vdevs');
this.numVdevsDisplay = document.getElementById('num-vdevs-display');
this.drivesPerVdevConfig = document.getElementById('drives-per-vdev-config');
this.drivesPerVdevSlider = document.getElementById('drives-per-vdev');
this.drivesPerVdevDisplay = document.getElementById('drives-per-vdev-display');
this.totalDrivesText = document.getElementById('total-drives-text');
}
attachEventListeners() {
this.numDrivesSlider.addEventListener('input', () => this.updateDriveDisplay());
this.mixedDrivesToggle.addEventListener('change', () => this.toggleMixedDrives());
this.calculateBtn.addEventListener('click', () => this.calculate());
this.raidTypeSelect.addEventListener('change', () => this.handleRaidTypeChange());
// ZFS vdev listeners
this.numVdevsSlider.addEventListener('input', () => this.updateVdevDisplay());
this.drivesPerVdevSlider.addEventListener('input', () => this.updateVdevDisplay());
}
handleRaidTypeChange() {
const raidType = this.raidTypeSelect.value;
const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType);
// Show/hide ZFS vdev configuration
this.zfsVdevConfig.style.display = isZFSVdev ? 'block' : 'none';
this.drivesPerVdevConfig.style.display = isZFSVdev ? 'block' : 'none';
// Show/hide regular drive count slider
const numDrivesGroup = this.numDrivesSlider.closest('.form-group');
numDrivesGroup.style.display = isZFSVdev ? 'none' : 'block';
if (isZFSVdev) {
this.updateVdevDisplay();
} else {
this.validateConfiguration();
}
}
updateVdevDisplay() {
const numVdevs = parseInt(this.numVdevsSlider.value);
const drivesPerVdev = parseInt(this.drivesPerVdevSlider.value);
const totalDrives = numVdevs * drivesPerVdev;
this.numVdevsDisplay.textContent = numVdevs;
this.drivesPerVdevDisplay.textContent = drivesPerVdev;
this.totalDrivesText.textContent = `Total drives: ${totalDrives}`;
// Update the main drive slider to match (for mixed drives feature)
this.numDrivesSlider.value = totalDrives;
this.numDrivesDisplay.textContent = totalDrives;
if (this.mixedDrivesToggle.checked) {
this.generateDriveInputs(totalDrives);
}
this.validateConfiguration();
}
updateDriveDisplay() {
const numDrives = parseInt(this.numDrivesSlider.value);
this.numDrivesDisplay.textContent = numDrives;
if (this.mixedDrivesToggle.checked) {
this.generateDriveInputs(numDrives);
}
this.validateConfiguration();
}
toggleMixedDrives() {
const isEnabled = this.mixedDrivesToggle.checked;
this.customDrivesContainer.style.display = isEnabled ? 'block' : 'none';
this.driveSizeSelect.disabled = isEnabled;
if (isEnabled) {
const raidType = this.raidTypeSelect.value;
const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType);
const numDrives = isZFSVdev ?
parseInt(this.numVdevsSlider.value) * parseInt(this.drivesPerVdevSlider.value) :
parseInt(this.numDrivesSlider.value);
this.generateDriveInputs(numDrives);
}
}
generateDriveInputs(numDrives) {
this.driveInputsContainer.innerHTML = '';
const driveSizes = [1, 2, 3, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24];
for (let i = 0; i < numDrives; i++) {
const row = document.createElement('div');
row.className = 'drive-input-row';
const label = document.createElement('span');
label.className = 'drive-label';
label.textContent = `Drive ${i + 1}:`;
const select = document.createElement('select');
select.className = 'drive-select';
select.id = `drive-${i}`;
driveSizes.forEach(size => {
const option = document.createElement('option');
option.value = size;
option.textContent = `${size} TB`;
if (size === 12) option.selected = true;
select.appendChild(option);
});
row.appendChild(label);
row.appendChild(select);
this.driveInputsContainer.appendChild(row);
}
}
validateConfiguration() {
const raidType = this.raidTypeSelect.value;
const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType);
const numDrives = isZFSVdev ?
parseInt(this.numVdevsSlider.value) * parseInt(this.drivesPerVdevSlider.value) :
parseInt(this.numDrivesSlider.value);
const config = RAID_CONFIGS[raidType];
if (numDrives < config.minDrives) {
this.calculateBtn.disabled = true;
this.calculateBtn.textContent = `Requires ${config.minDrives}+ drives`;
} else if (raidType === 'raid10' && numDrives % 2 !== 0) {
this.calculateBtn.disabled = true;
this.calculateBtn.textContent = 'RAID 10 requires even number of drives';
} else {
this.calculateBtn.disabled = false;
this.calculateBtn.innerHTML = `
<svg class="btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
Calculate Storage
`;
}
}
getDriveSizes() {
const raidType = this.raidTypeSelect.value;
const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType);
const numDrives = isZFSVdev ?
parseInt(this.numVdevsSlider.value) * parseInt(this.drivesPerVdevSlider.value) :
parseInt(this.numDrivesSlider.value);
if (this.mixedDrivesToggle.checked) {
const drives = [];
for (let i = 0; i < numDrives; i++) {
const select = document.getElementById(`drive-${i}`);
drives.push(parseFloat(select.value));
}
return drives;
} else {
const driveSize = parseFloat(this.driveSizeSelect.value);
return Array(numDrives).fill(driveSize);
}
}
calculate() {
try {
const raidType = this.raidTypeSelect.value;
const drives = this.getDriveSizes();
const config = RAID_CONFIGS[raidType];
// Check if this is a ZFS vdev configuration
const isZFSVdev = ['zfs-mirror', 'raidz1', 'raidz2', 'raidz3'].includes(raidType);
let result;
if (isZFSVdev) {
const numVdevs = parseInt(this.numVdevsSlider.value);
const drivesPerVdev = parseInt(this.drivesPerVdevSlider.value);
result = calculateZFSVdevStorage(raidType, drives, numVdevs, drivesPerVdev);
result.numVdevs = numVdevs;
result.drivesPerVdev = drivesPerVdev;
} else {
result = calculateRAIDStorage(raidType, drives);
}
this.displayResults(raidType, config, drives, result);
} catch (error) {
this.displayError(error.message);
}
}
displayResults(raidType, config, drives, result) {
const { usableCapacity, rawCapacity, efficiency, wastedSpace } = result;
const html = `
<div class="result-stats fade-in">
<div class="stat-card">
<div class="stat-label">Usable Capacity</div>
<div class="stat-value primary">${formatCapacity(usableCapacity)}</div>
<div class="stat-subtext">Actual storage available for data</div>
</div>
<div class="stat-card">
<div class="stat-label">Raw Capacity</div>
<div class="stat-value">${formatCapacity(rawCapacity)}</div>
<div class="stat-subtext">Total physical storage</div>
</div>
<div class="stat-card">
<div class="stat-label">Storage Efficiency</div>
<div class="stat-value">${formatPercentage(efficiency)}</div>
<div class="stat-subtext">${formatCapacity(wastedSpace)} used for redundancy</div>
</div>
<div class="capacity-visualization">
<div class="capacity-bar">
<div class="capacity-fill" style="width: ${efficiency * 100}%">
${formatPercentage(efficiency)} Usable
</div>
</div>
<div class="capacity-legend">
<span>0 TiB</span>
<span>${formatCapacity(rawCapacity, false)}</span>
</div>
</div>
<ul class="feature-list">
<li class="feature-item">
<svg class="feature-icon info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="feature-text">
<div class="feature-title">${config.name}</div>
<div class="feature-description">${config.description}</div>
</div>
</li>
<li class="feature-item">
<svg class="feature-icon ${config.faultTolerance > 0 ? 'success' : 'warning'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
<div class="feature-text">
<div class="feature-title">Fault Tolerance</div>
<div class="feature-description">
${config.faultTolerance > 0
? `Can survive ${config.faultTolerance} drive failure${config.faultTolerance > 1 ? 's' : ''} without data loss`
: 'No redundancy - any drive failure results in data loss'}
</div>
</div>
</li>
<li class="feature-item">
<svg class="feature-icon info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<div class="feature-text">
<div class="feature-title">Performance</div>
<div class="feature-description">
Read: ${config.readPerformance} | Write: ${config.writePerformance}
</div>
</div>
</li>
<li class="feature-item">
<svg class="feature-icon success" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="4" rx="1"/>
<rect x="2" y="10" width="20" height="4" rx="1"/>
<rect x="2" y="17" width="20" height="4" rx="1"/>
</svg>
<div class="feature-text">
<div class="feature-title">Drive Configuration</div>
<div class="feature-description">
${drives.length} drive${drives.length > 1 ? 's' : ''}: ${this.formatDriveList(drives)}
${result.numVdevs ? `<br><strong>${result.numVdevs} vdevs</strong> × ${result.drivesPerVdev} drives each` : ''}
</div>
</div>
</li>
<li class="feature-item">
<svg class="feature-icon info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"/>
</svg>
<div class="feature-text">
<div class="feature-title">Binary vs Decimal</div>
<div class="feature-description">
Calculations account for the difference between advertised TB (decimal) and actual TiB (binary) capacity
</div>
</div>
</li>
</ul>
</div>
`;
this.resultsContent.innerHTML = html;
}
formatDriveList(drives) {
const counts = {};
drives.forEach(size => {
counts[size] = (counts[size] || 0) + 1;
});
return Object.entries(counts)
.map(([size, count]) => `${count}x ${size}TB`)
.join(', ');
}
displayError(message) {
this.resultsContent.innerHTML = `
<div class="empty-state fade-in">
<svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="stroke: var(--color-accent-danger);">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<p style="color: var(--color-accent-danger);">${message}</p>
</div>
`;
}
}
// ===================================
// Initialize Application
// ===================================
document.addEventListener('DOMContentLoaded', () => {
new RAIDCalculator();
});