// API Configuration is loaded from config.js // If API_CONFIG is not defined, the config.js file is missing if (typeof API_CONFIG === 'undefined') { console.error('API_CONFIG is not defined. Please copy config.example.js to config.js and configure your credentials.'); alert('Configuration Error: config.js file is missing. Please check the console for details.'); } // GraphQL query executor async function executeGraphQLQuery(query) { try { const response = await fetch(API_CONFIG.serverUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-api-key': API_CONFIG.apiKey }, body: JSON.stringify({ query }) }); if (!response.ok) { const errorText = await response.text(); console.error('API Error:', errorText); throw new Error(`HTTP error! status: ${response.status}`); } const result = await response.json(); if (result.errors) { console.error('GraphQL errors:', result.errors); throw new Error('GraphQL query failed'); } return result.data; } catch (error) { console.error('API request failed:', error); throw error; } } // Fetch system metrics (CPU, Memory usage) async function fetchSystemMetrics() { const query = `query { metrics { cpu { percentTotal } memory { total used free percentTotal } } }`; const data = await executeGraphQLQuery(query); return data.metrics; } // Fetch array and disk information async function fetchArrayInfo() { const query = `query { array { state disks { name size status temp fsSize fsFree fsUsed } parityCheckStatus { status errors date } } }`; const data = await executeGraphQLQuery(query); return data.array; } // Fetch Docker container information async function fetchDockerContainers() { const query = `query { docker { containers { id names state status autoStart } } }`; const data = await executeGraphQLQuery(query); return data.docker.containers; } // Update timestamp function updateTimestamp() { 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', hour12: false }); document.getElementById('timestamp').textContent = formatted; } // Update CPU usage function updateCPU(percentage) { const progressBar = document.getElementById('cpu-progress'); const progressText = document.getElementById('cpu-text'); progressBar.style.width = percentage + '%'; progressText.textContent = percentage + '%'; // Apply color coding progressBar.classList.remove('warning', 'critical'); if (percentage >= 90) { progressBar.classList.add('critical'); } else if (percentage >= 70) { progressBar.classList.add('warning'); } } // Update Memory usage function updateMemory(percentage) { const progressBar = document.getElementById('memory-progress'); const progressText = document.getElementById('memory-text'); progressBar.style.width = percentage + '%'; progressText.textContent = percentage + '%'; // Apply color coding progressBar.classList.remove('warning', 'critical'); if (percentage >= 90) { progressBar.classList.add('critical'); } else if (percentage >= 70) { progressBar.classList.add('warning'); } } // Update Disk Ring Chart function updateDiskRingChart(percentage, usedKB, totalKB) { const ring = document.getElementById('disk-ring'); const text = document.getElementById('disk-percentage'); const fraction = document.getElementById('disk-fraction'); // SVG circle circumference calculation: 2 * PI * radius (radius = 80) const circumference = 2 * Math.PI * 80; // 502.65 const offset = circumference - (percentage / 100) * circumference; ring.style.strokeDashoffset = offset; text.textContent = percentage + '%'; // Calculate and display fraction (convert KB to TB, rounded to 2 decimals) const usedTB = (usedKB / 1024 / 1024 / 1024).toFixed(2); const totalTB = (totalKB / 1024 / 1024 / 1024).toFixed(2); fraction.textContent = `${usedTB} / ${totalTB} TB`; // Change color based on usage if (percentage >= 90) { ring.style.stroke = '#ffffff'; text.style.fill = '#ffffff'; fraction.style.fill = '#ffffff'; } else if (percentage >= 70) { ring.style.stroke = '#ff00ff'; text.style.fill = '#ff00ff'; fraction.style.fill = '#ff00ff'; } else { ring.style.stroke = '#00ffff'; text.style.fill = '#00ffff'; fraction.style.fill = '#00ffff'; } } // Update Photos Ring Chart (Disk 7) function updatePhotosRingChart(percentage, usedKB, totalKB) { const ring = document.getElementById('photos-ring'); const text = document.getElementById('photos-percentage'); const fraction = document.getElementById('photos-fraction'); const circumference = 2 * Math.PI * 80; const offset = circumference - (percentage / 100) * circumference; ring.style.strokeDashoffset = offset; text.textContent = percentage + '%'; const usedTB = (usedKB / 1024 / 1024 / 1024).toFixed(2); const totalTB = (totalKB / 1024 / 1024 / 1024).toFixed(2); fraction.textContent = `${usedTB} / ${totalTB} TB`; // Change color based on usage if (percentage >= 90) { ring.style.stroke = '#ffffff'; text.style.fill = '#ffffff'; fraction.style.fill = '#ffffff'; } else if (percentage >= 70) { ring.style.stroke = '#ff00ff'; text.style.fill = '#ff00ff'; fraction.style.fill = '#ff00ff'; } else { ring.style.stroke = '#00ffff'; text.style.fill = '#00ffff'; fraction.style.fill = '#00ffff'; } } // Update Media Ring Chart (All disks except 6 and 7) function updateMediaRingChart(percentage, usedKB, totalKB) { const ring = document.getElementById('media-ring'); const text = document.getElementById('media-percentage'); const fraction = document.getElementById('media-fraction'); const circumference = 2 * Math.PI * 80; const offset = circumference - (percentage / 100) * circumference; ring.style.strokeDashoffset = offset; text.textContent = percentage + '%'; const usedTB = (usedKB / 1024 / 1024 / 1024).toFixed(2); const totalTB = (totalKB / 1024 / 1024 / 1024).toFixed(2); fraction.textContent = `${usedTB} / ${totalTB} TB`; // Change color based on usage if (percentage >= 90) { ring.style.stroke = '#ffffff'; text.style.fill = '#ffffff'; fraction.style.fill = '#ffffff'; } else if (percentage >= 70) { ring.style.stroke = '#ff00ff'; text.style.fill = '#ff00ff'; fraction.style.fill = '#ff00ff'; } else { ring.style.stroke = '#00ffff'; text.style.fill = '#00ffff'; fraction.style.fill = '#00ffff'; } } // Update Docker Ring Chart (Disk 6) function updateDockerRingChart(percentage, usedKB, totalKB) { const ring = document.getElementById('docker-ring'); const text = document.getElementById('docker-percentage'); const fraction = document.getElementById('docker-fraction'); const circumference = 2 * Math.PI * 80; const offset = circumference - (percentage / 100) * circumference; ring.style.strokeDashoffset = offset; text.textContent = percentage + '%'; const usedTB = (usedKB / 1024 / 1024 / 1024).toFixed(2); const totalTB = (totalKB / 1024 / 1024 / 1024).toFixed(2); fraction.textContent = `${usedTB} / ${totalTB} TB`; // Change color based on usage if (percentage >= 90) { ring.style.stroke = '#ffffff'; text.style.fill = '#ffffff'; fraction.style.fill = '#ffffff'; } else if (percentage >= 70) { ring.style.stroke = '#ff00ff'; text.style.fill = '#ff00ff'; fraction.style.fill = '#ff00ff'; } else { ring.style.stroke = '#00ffff'; text.style.fill = '#00ffff'; fraction.style.fill = '#00ffff'; } } // Update Parity status function updateParity(parityData) { const statusElement = document.getElementById('parity-status'); const errorsElement = document.getElementById('parity-errors'); statusElement.textContent = parityData.status; errorsElement.textContent = parityData.errors; if (parityData.status !== 'VALID' || parityData.errors > 0) { statusElement.classList.add('error'); errorsElement.classList.add('error'); } else { statusElement.classList.remove('error'); errorsElement.classList.remove('error'); } document.getElementById('parity-last-check').textContent = parityData.lastCheck; } // Update Disk Array function updateDisks(disks) { const container = document.getElementById('disk-container'); container.innerHTML = ''; disks.forEach(disk => { const diskElement = document.createElement('div'); diskElement.className = 'disk-item'; const usedPercentage = disk.used; let progressClass = ''; if (usedPercentage >= 90) { progressClass = 'critical'; } else if (usedPercentage >= 80) { progressClass = 'warning'; } // Add label for disk6 and disk7 let diskLabel = disk.name; if (disk.name === 'disk6') { diskLabel = disk.name + ' (docker)'; } else if (disk.name === 'disk7') { diskLabel = disk.name + ' (photos)'; } diskElement.innerHTML = `
${diskLabel} ${disk.size} | ${disk.temperature}
${usedPercentage}% USED
`; container.appendChild(diskElement); }); } // Update Docker Containers function updateDocker(containers) { const container = document.getElementById('docker-container'); const gridDiv = document.createElement('div'); gridDiv.className = 'docker-grid'; containers.forEach(docker => { const dockerElement = document.createElement('div'); dockerElement.className = 'docker-item'; dockerElement.innerHTML = `
${docker.name}
${docker.status.toUpperCase()}
`; gridDiv.appendChild(dockerElement); }); container.innerHTML = ''; container.appendChild(gridDiv); } // Process and update all dashboard data async function updateDashboard() { try { // Fetch all data in parallel const [metrics, arrayInfo, dockerContainers] = await Promise.all([ fetchSystemMetrics(), fetchArrayInfo(), fetchDockerContainers() ]); // Update CPU usage from metrics if (metrics.cpu && metrics.cpu.percentTotal !== undefined) { const cpuUsage = Math.round(metrics.cpu.percentTotal); updateCPU(cpuUsage); } // Update Memory usage from metrics if (metrics.memory && metrics.memory.percentTotal !== undefined) { const memoryPercentage = Math.round(metrics.memory.percentTotal); updateMemory(memoryPercentage); } // Update Parity status if (arrayInfo.parityCheckStatus) { const parity = arrayInfo.parityCheckStatus; const parityData = { status: parity.status || 'UNKNOWN', lastCheck: parity.date || 'N/A', errors: parity.errors || 0 }; updateParity(parityData); } // Update Disks if (arrayInfo.disks && arrayInfo.disks.length > 0) { console.log('Processing disks:', arrayInfo.disks); // Calculate total disk usage across all disks let totalCapacity = 0; let totalUsed = 0; let dockerCapacity = 0; let dockerUsed = 0; let photosCapacity = 0; let photosUsed = 0; let mediaCapacity = 0; let mediaUsed = 0; const disksData = arrayInfo.disks.map(disk => { // Use filesystem data from the disk itself (fsSize, fsUsed are in KB) const total = disk.fsSize || disk.size || 0; const used = disk.fsUsed || 0; const usedPercentage = total > 0 ? Math.round((used / total) * 100) : 0; // Accumulate totals for ring charts totalCapacity += total; totalUsed += used; // Docker = disk6 if (disk.name === 'disk6') { dockerCapacity = total; dockerUsed = used; } // Photos = disk7 if (disk.name === 'disk7') { photosCapacity = total; photosUsed = used; } // Media = all disks except disk6 and disk7 if (disk.name !== 'disk6' && disk.name !== 'disk7') { mediaCapacity += total; mediaUsed += used; } // Handle temperature - could be null, number, or NaN let temperature = 'N/A'; if (disk.temp !== null && disk.temp !== undefined && !isNaN(disk.temp)) { temperature = disk.temp + '°C'; } return { name: disk.name || 'Unknown', size: formatBytes(total * 1024), // Convert KB to bytes for formatting used: usedPercentage, temperature: temperature }; }); // Update ring charts const totalUsagePercentage = totalCapacity > 0 ? Math.round((totalUsed / totalCapacity) * 100) : 0; console.log('Total usage:', { totalUsagePercentage, totalUsed, totalCapacity }); updateDiskRingChart(totalUsagePercentage, totalUsed, totalCapacity); const dockerUsagePercentage = dockerCapacity > 0 ? Math.round((dockerUsed / dockerCapacity) * 100) : 0; console.log('Docker usage:', { dockerUsagePercentage, dockerUsed, dockerCapacity }); updateDockerRingChart(dockerUsagePercentage, dockerUsed, dockerCapacity); const photosUsagePercentage = photosCapacity > 0 ? Math.round((photosUsed / photosCapacity) * 100) : 0; console.log('Photos usage:', { photosUsagePercentage, photosUsed, photosCapacity }); updatePhotosRingChart(photosUsagePercentage, photosUsed, photosCapacity); const mediaUsagePercentage = mediaCapacity > 0 ? Math.round((mediaUsed / mediaCapacity) * 100) : 0; console.log('Media usage:', { mediaUsagePercentage, mediaUsed, mediaCapacity }); updateMediaRingChart(mediaUsagePercentage, mediaUsed, mediaCapacity); console.log('Processed disk data:', disksData); updateDisks(disksData); } else { console.log('No disks data available'); } // Update Docker containers if (dockerContainers && dockerContainers.length > 0) { console.log('Processing docker containers:', dockerContainers); const dockerData = dockerContainers.map(container => { // Extract container name (remove leading slash if present) const name = container.names[0] ? container.names[0].replace(/^\//, '') : 'Unknown'; let state = container.state.toLowerCase(); // Map "exited" to "offline" if (state === 'exited') { state = 'offline'; } return { name: name, status: state }; }); console.log('Processed docker data:', dockerData); updateDocker(dockerData); } else { console.log('No docker containers available'); } } catch (error) { console.error('Dashboard update failed:', error); // Show error state in UI showError('Failed to fetch data from Unraid server. Retrying...'); } } // Format bytes to human-readable format function formatBytes(bytes) { if (!bytes || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; } // Show error message function showError(message) { const timestamp = document.getElementById('timestamp'); if (timestamp) { timestamp.textContent = `ERROR: ${message}`; timestamp.style.color = '#ffffff'; } } // Initialize dashboard document.addEventListener('DOMContentLoaded', () => { updateDashboard(); // Update every 5 seconds setInterval(updateDashboard, 5000); });