Add Docker deployment for Unraid
- Dockerfile with nginx:alpine for static file serving - docker-compose.yml with port 9113:80 mapping - deploy.sh for git-based deployment to Unraid - setup-unraid.sh for initial server configuration - manage.sh for container operations (logs, status, restart, etc.) - .gitignore to exclude config.js Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
401
script.js
401
script.js
@@ -5,6 +5,125 @@ if (typeof API_CONFIG === 'undefined') {
|
||||
alert('Configuration Error: config.js file is missing. Please check the console for details.');
|
||||
}
|
||||
|
||||
// Theme Toggle Functionality
|
||||
const ThemeToggle = {
|
||||
init() {
|
||||
console.log('Theme toggle initializing...');
|
||||
// Load saved theme preference or default to dark mode
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
console.log('Saved theme:', savedTheme);
|
||||
this.setTheme(savedTheme);
|
||||
|
||||
// Add click event listener to toggle button
|
||||
const toggleButton = document.getElementById('theme-toggle');
|
||||
console.log('Toggle button found:', toggleButton);
|
||||
if (toggleButton) {
|
||||
toggleButton.addEventListener('click', () => {
|
||||
console.log('Theme toggle clicked!');
|
||||
this.toggleTheme();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setTheme(theme) {
|
||||
console.log('Setting theme to:', theme);
|
||||
const body = document.body;
|
||||
const toggleButton = document.getElementById('theme-toggle');
|
||||
|
||||
if (theme === 'light') {
|
||||
body.classList.add('light-mode');
|
||||
console.log('Light mode activated - body classes:', body.className);
|
||||
if (toggleButton) toggleButton.textContent = 'DARK MODE';
|
||||
} else {
|
||||
body.classList.remove('light-mode');
|
||||
console.log('Dark mode activated - body classes:', body.className);
|
||||
if (toggleButton) toggleButton.textContent = 'LIGHT MODE';
|
||||
}
|
||||
|
||||
localStorage.setItem('theme', theme);
|
||||
},
|
||||
|
||||
toggleTheme() {
|
||||
const currentTheme = document.body.classList.contains('light-mode') ? 'light' : 'dark';
|
||||
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||
this.setTheme(newTheme);
|
||||
|
||||
// Refresh dashboard to update colors
|
||||
if (typeof updateDashboard === 'function') {
|
||||
updateDashboard();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize theme on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
ThemeToggle.init();
|
||||
});
|
||||
|
||||
// Theme-aware color helper
|
||||
function getThemeColors() {
|
||||
const isLightMode = document.body.classList.contains('light-mode');
|
||||
|
||||
if (isLightMode) {
|
||||
return {
|
||||
normal: '#0066cc', // Blue for normal usage
|
||||
warning: '#f0ad4e', // Orange for warning
|
||||
critical: '#d9534f' // Red for critical
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
normal: '#00ffff', // Cyan for normal usage
|
||||
warning: '#ff00ff', // Magenta for warning
|
||||
critical: '#ffffff' // White for critical
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Historical data tracking
|
||||
const HistoricalData = {
|
||||
maxDataPoints: 20, // Keep last 20 data points
|
||||
cpu: [],
|
||||
memory: [],
|
||||
disks: {},
|
||||
|
||||
addCPUData(percentage) {
|
||||
this.cpu.push({ timestamp: Date.now(), value: percentage });
|
||||
if (this.cpu.length > this.maxDataPoints) {
|
||||
this.cpu.shift();
|
||||
}
|
||||
},
|
||||
|
||||
addMemoryData(percentage) {
|
||||
this.memory.push({ timestamp: Date.now(), value: percentage });
|
||||
if (this.memory.length > this.maxDataPoints) {
|
||||
this.memory.shift();
|
||||
}
|
||||
},
|
||||
|
||||
addDiskData(diskName, percentage) {
|
||||
if (!this.disks[diskName]) {
|
||||
this.disks[diskName] = [];
|
||||
}
|
||||
this.disks[diskName].push({ timestamp: Date.now(), value: percentage });
|
||||
if (this.disks[diskName].length > this.maxDataPoints) {
|
||||
this.disks[diskName].shift();
|
||||
}
|
||||
},
|
||||
|
||||
getTrend(dataArray) {
|
||||
if (dataArray.length < 2) return 'stable';
|
||||
|
||||
const recent = dataArray.slice(-3);
|
||||
const avg = recent.reduce((sum, item) => sum + item.value, 0) / recent.length;
|
||||
const oldest = recent[0].value;
|
||||
|
||||
const change = avg - oldest;
|
||||
|
||||
if (Math.abs(change) < 1) return 'stable';
|
||||
return change > 0 ? 'up' : 'down';
|
||||
}
|
||||
};
|
||||
|
||||
// GraphQL query executor
|
||||
async function executeGraphQLQuery(query) {
|
||||
try {
|
||||
@@ -55,12 +174,136 @@ async function fetchArrayInfo() {
|
||||
|
||||
// Fetch Docker container information
|
||||
async function fetchDockerContainers() {
|
||||
const query = `query { docker { containers { id names state status autoStart } } }`;
|
||||
const query = `query { docker { containers { id names state status autoStart ports { privatePort publicPort type } } } }`;
|
||||
|
||||
const data = await executeGraphQLQuery(query);
|
||||
return data.docker.containers;
|
||||
}
|
||||
|
||||
// Helper function to create trend indicator HTML
|
||||
function createTrendIndicator(trend) {
|
||||
const icons = {
|
||||
up: '▲',
|
||||
down: '▼',
|
||||
stable: '━'
|
||||
};
|
||||
|
||||
return `<span class="trend-indicator trend-${trend}">${icons[trend]}</span>`;
|
||||
}
|
||||
|
||||
// Helper function to extract web UI ports from container ports
|
||||
function getWebUIPort(ports) {
|
||||
if (!ports || ports.length === 0) {
|
||||
console.log('No ports available');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('All ports:', JSON.stringify(ports));
|
||||
|
||||
// Common web UI ports (HTTP/HTTPS)
|
||||
const webPorts = [80, 443, 8080, 8443, 3000, 5000, 8000, 8888, 9000];
|
||||
|
||||
// Find TCP ports that are commonly used for web interfaces
|
||||
// Note: GraphQL returns type as uppercase "TCP" or "UDP"
|
||||
const tcpPorts = ports.filter(p => p.type === 'TCP' && p.publicPort);
|
||||
console.log('TCP ports with public mapping:', JSON.stringify(tcpPorts));
|
||||
|
||||
// Prioritize common web ports
|
||||
for (const port of webPorts) {
|
||||
const match = tcpPorts.find(p => p.privatePort === port);
|
||||
if (match) {
|
||||
console.log('Found common web port:', match.publicPort);
|
||||
return match.publicPort;
|
||||
}
|
||||
}
|
||||
|
||||
// If no common port found, return the first TCP port with a public mapping
|
||||
if (tcpPorts.length > 0) {
|
||||
console.log('Using first TCP port:', tcpPorts[0].publicPort);
|
||||
return tcpPorts[0].publicPort;
|
||||
}
|
||||
|
||||
console.log('No suitable port found');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Helper function to create a clickable web UI link
|
||||
function createWebUILink(port) {
|
||||
if (!port) return '';
|
||||
|
||||
// Use the Unraid server IP from config
|
||||
const serverHost = API_CONFIG.serverUrl.match(/http:\/\/([^:]+)/)[1];
|
||||
const url = `http://${serverHost}:${port}`;
|
||||
|
||||
return `<a href="${url}" target="_blank" class="webui-link" title="Open Web UI">:${port}</a>`;
|
||||
}
|
||||
|
||||
// Helper function to create tooltip HTML
|
||||
function createTooltip(title, data) {
|
||||
const rows = Object.entries(data).map(([label, value]) =>
|
||||
`<div class="tooltip-row">
|
||||
<span class="tooltip-label">${label}:</span>
|
||||
<span class="tooltip-value">${value}</span>
|
||||
</div>`
|
||||
).join('');
|
||||
|
||||
return `<div class="tooltip">
|
||||
<div class="tooltip-title">${title}</div>
|
||||
<div class="tooltip-content">
|
||||
${rows}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Generic ring chart update function to reduce code duplication
|
||||
function updateRingChartGeneric(elementId, containerId, title, percentage, usedKB, totalKB) {
|
||||
const ring = document.getElementById(`${elementId}-ring`);
|
||||
const text = document.getElementById(`${elementId}-percentage`);
|
||||
const fraction = document.getElementById(`${elementId}-fraction`);
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
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);
|
||||
const freeTB = (totalTB - usedTB).toFixed(2);
|
||||
fraction.textContent = `${usedTB} / ${totalTB} TB`;
|
||||
|
||||
// Add tooltip
|
||||
const existingTooltip = container.querySelector('.tooltip');
|
||||
if (existingTooltip) {
|
||||
existingTooltip.remove();
|
||||
}
|
||||
|
||||
const tooltipHTML = createTooltip(title, {
|
||||
'Used': `${usedTB} TB`,
|
||||
'Free': `${freeTB} TB`,
|
||||
'Total': `${totalTB} TB`,
|
||||
'Usage': `${percentage}%`
|
||||
});
|
||||
container.insertAdjacentHTML('beforeend', tooltipHTML);
|
||||
|
||||
// Change color based on usage (theme-aware)
|
||||
const colors = getThemeColors();
|
||||
let color;
|
||||
|
||||
if (percentage >= 90) {
|
||||
color = colors.critical;
|
||||
} else if (percentage >= 70) {
|
||||
color = colors.warning;
|
||||
} else {
|
||||
color = colors.normal;
|
||||
}
|
||||
|
||||
ring.style.stroke = color;
|
||||
text.style.fill = color;
|
||||
fraction.style.fill = color;
|
||||
}
|
||||
|
||||
// Update timestamp
|
||||
function updateTimestamp() {
|
||||
const now = new Date();
|
||||
@@ -81,8 +324,12 @@ function updateCPU(percentage) {
|
||||
const progressBar = document.getElementById('cpu-progress');
|
||||
const progressText = document.getElementById('cpu-text');
|
||||
|
||||
// Track historical data
|
||||
HistoricalData.addCPUData(percentage);
|
||||
const trend = HistoricalData.getTrend(HistoricalData.cpu);
|
||||
|
||||
progressBar.style.width = percentage + '%';
|
||||
progressText.textContent = percentage + '%';
|
||||
progressText.innerHTML = percentage + '%' + createTrendIndicator(trend);
|
||||
|
||||
// Apply color coding
|
||||
progressBar.classList.remove('warning', 'critical');
|
||||
@@ -98,8 +345,12 @@ function updateMemory(percentage) {
|
||||
const progressBar = document.getElementById('memory-progress');
|
||||
const progressText = document.getElementById('memory-text');
|
||||
|
||||
// Track historical data
|
||||
HistoricalData.addMemoryData(percentage);
|
||||
const trend = HistoricalData.getTrend(HistoricalData.memory);
|
||||
|
||||
progressBar.style.width = percentage + '%';
|
||||
progressText.textContent = percentage + '%';
|
||||
progressText.innerHTML = percentage + '%' + createTrendIndicator(trend);
|
||||
|
||||
// Apply color coding
|
||||
progressBar.classList.remove('warning', 'critical');
|
||||
@@ -112,132 +363,22 @@ function updateMemory(percentage) {
|
||||
|
||||
// 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';
|
||||
}
|
||||
updateRingChartGeneric('disk', 'disk-tooltip-container', 'Total Disk Usage', percentage, usedKB, totalKB);
|
||||
}
|
||||
|
||||
// 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';
|
||||
}
|
||||
updateRingChartGeneric('photos', 'photos-tooltip-container', 'Photos Storage (Disk 7)', percentage, usedKB, totalKB);
|
||||
}
|
||||
|
||||
// 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';
|
||||
}
|
||||
updateRingChartGeneric('media', 'media-tooltip-container', 'Media Storage', percentage, usedKB, totalKB);
|
||||
}
|
||||
|
||||
// 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';
|
||||
}
|
||||
updateRingChartGeneric('docker', 'docker-tooltip-container', 'Docker Storage (Disk 6)', percentage, usedKB, totalKB);
|
||||
}
|
||||
|
||||
// Update Parity status
|
||||
@@ -276,6 +417,10 @@ function updateDisks(disks) {
|
||||
progressClass = 'warning';
|
||||
}
|
||||
|
||||
// Track historical data for this disk
|
||||
HistoricalData.addDiskData(disk.name, usedPercentage);
|
||||
const trend = HistoricalData.getTrend(HistoricalData.disks[disk.name] || []);
|
||||
|
||||
// Add label for disk6 and disk7
|
||||
let diskLabel = disk.name;
|
||||
if (disk.name === 'disk6') {
|
||||
@@ -284,6 +429,14 @@ function updateDisks(disks) {
|
||||
diskLabel = disk.name + ' (photos)';
|
||||
}
|
||||
|
||||
// Create tooltip with disk details
|
||||
const diskTooltip = createTooltip(`${disk.name} Details`, {
|
||||
'Capacity': disk.size,
|
||||
'Temperature': disk.temperature,
|
||||
'Usage': `${usedPercentage}%`,
|
||||
'Status': disk.status || 'Active'
|
||||
});
|
||||
|
||||
diskElement.innerHTML = `
|
||||
<div class="disk-header">
|
||||
<span class="disk-name">${diskLabel}</span>
|
||||
@@ -291,8 +444,9 @@ function updateDisks(disks) {
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill ${progressClass}" style="width: ${usedPercentage}%"></div>
|
||||
<span class="progress-text">${usedPercentage}% USED</span>
|
||||
<span class="progress-text">${usedPercentage}% USED ${createTrendIndicator(trend)}</span>
|
||||
</div>
|
||||
${diskTooltip}
|
||||
`;
|
||||
|
||||
container.appendChild(diskElement);
|
||||
@@ -310,8 +464,14 @@ function updateDocker(containers) {
|
||||
const dockerElement = document.createElement('div');
|
||||
dockerElement.className = 'docker-item';
|
||||
|
||||
// Extract web UI port
|
||||
console.log('Container:', docker.name, 'Ports:', docker.ports);
|
||||
const webUIPort = getWebUIPort(docker.ports);
|
||||
console.log('Detected web UI port for', docker.name, ':', webUIPort);
|
||||
const portLink = createWebUILink(webUIPort);
|
||||
|
||||
dockerElement.innerHTML = `
|
||||
<div class="docker-name">${docker.name}</div>
|
||||
<div class="docker-name">${docker.name}${portLink}</div>
|
||||
<div class="docker-status ${docker.status}">${docker.status.toUpperCase()}</div>
|
||||
`;
|
||||
|
||||
@@ -449,7 +609,8 @@ async function updateDashboard() {
|
||||
|
||||
return {
|
||||
name: name,
|
||||
status: state
|
||||
status: state,
|
||||
ports: container.ports || []
|
||||
};
|
||||
});
|
||||
console.log('Processed docker data:', dockerData);
|
||||
|
||||
Reference in New Issue
Block a user