mirror of
https://github.com/DeNNiiInc/Advanced-Raid-Calculator.git
synced 2026-04-17 12:45:59 +00:00
719 lines
27 KiB
JavaScript
719 lines
27 KiB
JavaScript
// ===================================
|
||
// 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();
|
||
});
|