diff --git a/.gitignore b/.gitignore index a630026..c2c688d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ Thumbs.db # IDE files .vscode/ .idea/ +.claude/ *.swp *.swo *~ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..108a963 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM nginx:alpine +COPY index.html /usr/share/nginx/html/ +COPY styles.css /usr/share/nginx/html/ +COPY script.js /usr/share/nginx/html/ +COPY config.js /usr/share/nginx/html/ +RUN chmod 644 /usr/share/nginx/html/*.html /usr/share/nginx/html/*.css /usr/share/nginx/html/*.js +EXPOSE 80 diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md new file mode 100644 index 0000000..d635638 --- /dev/null +++ b/IMPROVEMENTS.md @@ -0,0 +1,680 @@ +# Dashboard Improvement Ideas + +This document contains recommended improvements for the Thanos Systems Monitor dashboard. + +## Status Legend +- ✅ **Completed**: Implementation finished +- 🔄 **In Progress**: Currently being worked on +- ⏳ **Pending**: Not yet started +- 💡 **Proposed**: Idea for consideration + +--- + +## 1. Security Concerns ✅ + +**Status**: COMPLETED (2025-11-22) + +**Implementation**: +- ✅ Created separate `config.js` file for API credentials +- ✅ Added `config.js` to `.gitignore` +- ✅ Created `config.example.js` template +- ✅ Updated `script.js` to remove hardcoded credentials +- ✅ Added validation check for missing configuration + +--- + +## 2. Error Handling and User Feedback ⏳ + +**Current Limitation**: The `showError()` function at `script.js:479-485` only displays errors in the timestamp element, which does not exist in your HTML. + +**Proposed Improvements**: + +### 2.1 Add Timestamp and Error Display +- Add timestamp element to header showing last update time +- Create dedicated error notification system +- Style error notifications with cyberpunk theme + +### 2.2 Enhanced Error Handling +- Implement retry logic with exponential backoff when API requests fail +- Display connection status indicator (online/offline/connecting) +- Show loading states during initial data fetch +- Add user-friendly error messages for common failures + +### 2.3 Implementation Details +```javascript +// Retry logic with exponential backoff +async function fetchWithRetry(fetchFunction, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await fetchFunction(); + } catch (error) { + if (i === maxRetries - 1) throw error; + await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000)); + } + } +} + +// Connection status indicator +function updateConnectionStatus(status) { + // 'online', 'offline', 'connecting', 'error' +} +``` + +--- + +## 3. Data Visualization Enhancements ✅ + +### 3.1 Ring Chart Improvements +**Status**: COMPLETED (2025-11-22) + +**Implemented Features**: +- ✅ Added smooth CSS transitions for ring chart value changes +- ✅ Display trend indicators (▲/▼/━ showing increasing/decreasing/stable usage) +- ✅ Added hover tooltips showing detailed information (Used, Free, Total, Usage %) +- ✅ Implemented historical data tracking (last 20 data points per metric) +- ✅ Enhanced hover effects with scale transformation +- ✅ Refactored code to use generic updateRingChartGeneric() function, reducing duplication + +**Implementation Details**: +- Created HistoricalData object to track CPU, Memory, and individual disk usage over time +- Added createTrendIndicator() helper function for visual trend display +- Added createTooltip() helper function for consistent tooltip formatting +- Tooltips display on hover with fade-in animation +- Trend calculation based on last 3 data points with 1% threshold for stability + +### 3.2 Disk Array Enhancements +**Status**: COMPLETED (2025-11-22) + +**Implemented Features**: +- ✅ Added hover tooltips for each disk showing capacity, temperature, usage, and status +- ✅ Added trend indicators to disk progress bars (▲/▼/━) +- ✅ Enhanced hover effects (background color change, border color change) +- ✅ Historical data tracking for individual disks + +**Remaining Enhancements**: +- ⏳ Visual indicators for disk health status (SMART data if available from API) +- ⏳ Show read/write speed metrics if available from API +- ⏳ Group disks by function (parity, cache, data) more clearly +- ⏳ Add disk age/hours powered on if available from API + +### 3.3 Historical Data Graphs +**New Feature**: +- Line graphs for CPU usage over time (last hour/day) +- Memory usage trends +- Disk I/O graphs +- Network traffic visualization +- Implement using HTML5 Canvas or lightweight charting library + +--- + +## 4. Performance Optimizations ⏳ + +**Current Implementation**: Updates occur every 5 seconds regardless of visibility + +### 4.1 Page Visibility API +```javascript +document.addEventListener('visibilitychange', () => { + if (document.hidden) { + // Pause updates + clearInterval(updateInterval); + } else { + // Resume updates + updateInterval = setInterval(updateDashboard, 5000); + updateDashboard(); // Immediate update on return + } +}); +``` + +### 4.2 Animation Optimization +- Use `requestAnimationFrame` for smoother animations +- Batch DOM updates to minimize reflows +- Use CSS transforms instead of position changes + +### 4.3 Differential Updates +- Only update DOM elements that have changed values +- Compare previous state with current state +- Reduce unnecessary re-renders + +### 4.4 Web Workers +- Move data processing to Web Worker +- Keep UI thread responsive +- Process API responses in background + +--- + +## 5. Code Quality Improvements ⏳ + +### 5.1 Eliminate Code Duplication + +**Current Issue**: Ring chart update functions (`script.js:113-240`) contain significant duplication + +**Refactoring Recommendation**: +```javascript +function updateRingChart(elementId, percentage, usedKB, totalKB) { + const ring = document.getElementById(`${elementId}-ring`); + const text = document.getElementById(`${elementId}-percentage`); + const fraction = document.getElementById(`${elementId}-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`; + + // Apply color coding + applyThresholdColors(ring, text, fraction, percentage); +} + +function applyThresholdColors(ring, text, fraction, percentage) { + const color = percentage >= 90 ? '#ffffff' : + percentage >= 70 ? '#ff00ff' : '#00ffff'; + + ring.style.stroke = color; + text.style.fill = color; + fraction.style.fill = color; +} + +// Usage +updateRingChart('disk', totalUsagePercentage, totalUsed, totalCapacity); +updateRingChart('docker', dockerUsagePercentage, dockerUsed, dockerCapacity); +updateRingChart('photos', photosUsagePercentage, photosUsed, photosCapacity); +updateRingChart('media', mediaUsagePercentage, mediaUsed, mediaCapacity); +``` + +### 5.2 Extract Constants +```javascript +// Configuration constants +const CONSTANTS = { + UPDATE_INTERVAL: 5000, + RING_RADIUS: 80, + WARNING_THRESHOLD: 70, + CRITICAL_THRESHOLD: 90, + COLORS: { + PRIMARY: '#00ffff', + WARNING: '#ff00ff', + CRITICAL: '#ffffff', + BACKGROUND: '#1a1a1a' + } +}; +``` + +### 5.3 Add JSDoc Documentation +```javascript +/** + * Updates a ring chart with usage data + * @param {string} elementId - The base ID for the ring chart elements + * @param {number} percentage - Usage percentage (0-100) + * @param {number} usedKB - Used space in kilobytes + * @param {number} totalKB - Total space in kilobytes + */ +function updateRingChart(elementId, percentage, usedKB, totalKB) { + // Implementation +} +``` + +### 5.4 Error Boundaries +- Add try-catch blocks around each update function +- Prevent one failing component from breaking entire dashboard +- Log errors for debugging + +### 5.5 Data Validation +```javascript +function validateMetricsData(data) { + if (!data || typeof data !== 'object') { + throw new Error('Invalid metrics data'); + } + + if (data.cpu && typeof data.cpu.percentTotal !== 'number') { + console.warn('Invalid CPU data:', data.cpu); + } + + return data; +} +``` + +--- + +## 6. Responsive Design Enhancements ⏳ + +**Current State**: Basic responsive design at 768px breakpoint + +### 6.1 Additional Breakpoints +```css +/* Tablet landscape */ +@media (max-width: 1024px) { + .grid-row-1 { + grid-template-columns: 1fr; + } + + .disk-usage-layout { + grid-template-columns: 1fr; + } +} + +/* Tablet portrait */ +@media (max-width: 768px) { + .ring-chart-row-split { + grid-template-columns: 1fr; + } +} + +/* Mobile */ +@media (max-width: 480px) { + .mini-ring-chart { + width: 120px; + height: 120px; + } + + .title { + font-size: 1.5rem; + } +} +``` + +### 6.2 Raspberry Pi Display Optimization +- Test on actual Raspberry Pi display resolution +- Optimize for common resolutions: 1920x1080, 1280x720 +- Consider touch-friendly interface elements +- Test browser performance on Raspberry Pi 3 + +### 6.3 Orientation Support +- Handle landscape and portrait orientations +- Reflow layout appropriately for vertical displays + +--- + +## 7. Accessibility Improvements ⏳ + +**Current State**: Limited accessibility features + +### 7.1 ARIA Labels and Roles +```html + +
+
+ 75% +
+ + +
+ PARITY STATUS: + VALID +
+``` + +### 7.2 Live Regions +```html + +
+ +
+``` + +### 7.3 Keyboard Navigation +- Add tab index to interactive elements +- Implement keyboard shortcuts for common actions +- Ensure focus indicators are visible + +### 7.4 Color Contrast +- Verify all text meets WCAG AA standards (4.5:1 ratio) +- Current cyan (#00ffff) on black (#000000) has 15.3:1 ratio ✓ +- Test magenta (#ff00ff) and white (#ffffff) combinations +- Consider adding high contrast mode option + +### 7.5 Screen Reader Support +```javascript +function announceUpdate(message) { + const announcer = document.getElementById('status-announcer'); + announcer.textContent = message; +} + +// Usage +announceUpdate('CPU usage increased to 85%'); +announceUpdate('Docker container "plex" started'); +``` + +--- + +## 8. Additional Features 💡 + +### 8.1 User Preferences +**Proposed Implementation**: +```javascript +const UserPreferences = { + updateInterval: 5000, + theme: 'dark', // Future: 'light' option + showAnimations: true, + temperatureUnit: 'celsius', // or 'fahrenheit' + notifications: { + enabled: true, + cpuThreshold: 90, + memoryThreshold: 90, + diskThreshold: 90 + } +}; + +// Save to localStorage +function savePreferences(prefs) { + localStorage.setItem('dashboardPrefs', JSON.stringify(prefs)); +} + +// Load from localStorage +function loadPreferences() { + const saved = localStorage.getItem('dashboardPrefs'); + return saved ? JSON.parse(saved) : UserPreferences; +} +``` + +### 8.2 Configurable Alerts +- Set custom thresholds for warnings +- Browser notifications for critical states +- Email/webhook notifications (requires backend) +- Sound alerts for critical conditions + +### 8.3 Data Export +```javascript +function exportData(format = 'json') { + const data = { + timestamp: new Date().toISOString(), + cpu: currentCPUUsage, + memory: currentMemoryUsage, + disks: currentDiskData, + docker: currentDockerData + }; + + if (format === 'json') { + const blob = new Blob([JSON.stringify(data, null, 2)], + { type: 'application/json' }); + downloadFile(blob, `unraid-metrics-${Date.now()}.json`); + } else if (format === 'csv') { + // Convert to CSV and download + } +} +``` + +### 8.4 Theme Options +- Light theme option +- Custom color schemes +- Adjustable glow intensity +- Disable animations option (for performance) + +### 8.5 Network Monitoring +**If Available from API**: +- Network interface statistics +- Upload/download speeds +- Total bandwidth usage +- Per-container network usage + +### 8.6 UPS Status +**If Available from API**: +- Battery charge level +- Runtime remaining +- Power status (on battery/on AC) +- UPS load percentage + +### 8.7 System Temperature Monitoring +**If Available from API**: +- CPU temperature +- Motherboard temperature +- Additional thermal sensors +- Temperature history graphs + +### 8.8 Cache Drive Monitoring +- Separate cache drive statistics +- Cache utilization +- Mover status and schedule + +--- + +## 9. Docker Container Enhancements ⏳ + +**Current Display**: Basic name and status + +### 9.1 Additional Container Information +**Proposed Additions**: +- CPU usage per container (if available) +- Memory usage per container (if available) +- Port mappings display +- Container uptime +- Container health check status +- Image version/tag + +### 9.2 Container Controls +**If API Supports**: +```javascript +async function controlContainer(containerId, action) { + // action: 'start', 'stop', 'restart' + const query = `mutation { + dockerContainerControl(id: "${containerId}", action: "${action}") { + success + message + } + }`; + + return await executeGraphQLQuery(query); +} +``` + +**UI Implementation**: +- Add start/stop/restart buttons to each container card +- Confirmation dialog for destructive actions +- Loading state during action execution +- Success/error feedback + +### 9.3 Container Details Modal +- Click container to view detailed information +- Show logs (last 100 lines) +- Environment variables +- Volume mounts +- Network configuration + +--- + +## 10. Testing and Monitoring ⏳ + +### 10.1 Unit Tests +**Proposed Framework**: Jest or Vitest (lightweight) + +```javascript +// Example test file: script.test.js +describe('formatBytes', () => { + test('formats bytes correctly', () => { + expect(formatBytes(0)).toBe('0 B'); + expect(formatBytes(1024)).toBe('1 KB'); + expect(formatBytes(1048576)).toBe('1 MB'); + expect(formatBytes(1073741824)).toBe('1 GB'); + }); +}); + +describe('updateCPU', () => { + test('applies critical class above 90%', () => { + document.body.innerHTML = ` +
+ + `; + + updateCPU(95); + + const progressBar = document.getElementById('cpu-progress'); + expect(progressBar.classList.contains('critical')).toBe(true); + }); +}); +``` + +### 10.2 Integration Tests +**Test API Integration**: +```javascript +describe('API Integration', () => { + test('fetches system metrics successfully', async () => { + const metrics = await fetchSystemMetrics(); + expect(metrics).toHaveProperty('cpu'); + expect(metrics).toHaveProperty('memory'); + }); + + test('handles API errors gracefully', async () => { + // Mock failed request + global.fetch = jest.fn(() => Promise.reject('API Error')); + + await expect(fetchSystemMetrics()).rejects.toThrow(); + }); +}); +``` + +### 10.3 Performance Monitoring +```javascript +// Add performance tracking +const performanceMetrics = { + apiCallDuration: [], + renderDuration: [], + totalUpdateDuration: [] +}; + +async function updateDashboard() { + const startTime = performance.now(); + + try { + // Existing update logic + } finally { + const duration = performance.now() - startTime; + performanceMetrics.totalUpdateDuration.push(duration); + + // Log if update takes too long + if (duration > 1000) { + console.warn(`Slow update: ${duration}ms`); + } + } +} +``` + +### 10.4 Error Logging +**Client-Side Error Tracking**: +```javascript +// Simple error logger +const ErrorLogger = { + errors: [], + + log(error, context = {}) { + const errorEntry = { + timestamp: new Date().toISOString(), + message: error.message, + stack: error.stack, + context: context + }; + + this.errors.push(errorEntry); + console.error('Error logged:', errorEntry); + + // Optional: Send to remote logging service + // this.sendToRemote(errorEntry); + }, + + getErrors() { + return this.errors; + }, + + clearErrors() { + this.errors = []; + } +}; + +// Global error handler +window.addEventListener('error', (event) => { + ErrorLogger.log(event.error, { + type: 'uncaught', + filename: event.filename, + lineno: event.lineno + }); +}); +``` + +### 10.5 Health Check Endpoint +**If you add a service worker**: +```javascript +// Monitor dashboard health +function dashboardHealthCheck() { + return { + status: 'healthy', + lastUpdate: lastUpdateTimestamp, + apiConnected: apiConnectionStatus, + errorCount: ErrorLogger.errors.length, + uptime: performance.now() + }; +} +``` + +--- + +## 11. Advanced Features 💡 + +### 11.1 Mobile App +- Progressive Web App (PWA) support +- Add manifest.json +- Service worker for offline capability +- App install prompt + +### 11.2 Multi-Server Support +- Monitor multiple Unraid servers +- Server selector dropdown +- Aggregate statistics +- Server comparison view + +### 11.3 Custom Widgets +- Drag-and-drop dashboard customization +- User-configurable layout +- Widget library (add/remove sections) +- Save custom layouts + +### 11.4 Scheduled Reports +- Daily/weekly summary emails +- PDF report generation +- Usage trends analysis +- Capacity planning recommendations + +--- + +## Implementation Priority + +### High Priority (Core Functionality) +1. ✅ Security improvements +2. Error handling and user feedback +3. Code quality improvements (reduce duplication) +4. Performance optimizations (Page Visibility API) + +### Medium Priority (User Experience) +5. Docker container enhancements +6. Data visualization improvements +7. Responsive design enhancements +8. Accessibility improvements + +### Low Priority (Nice to Have) +9. Additional features (themes, preferences) +10. Testing infrastructure +11. Advanced features (PWA, multi-server) + +--- + +## Notes + +- All improvements should maintain the cyberpunk aesthetic +- Keep implementation lightweight for Raspberry Pi 3 performance +- Test thoroughly on actual hardware before deployment +- Document all configuration options +- Maintain backward compatibility where possible + +--- + +**Last Updated**: 2025-11-22 +**Status**: Living document - update as improvements are implemented diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..72c962f --- /dev/null +++ b/deploy.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# deploy.sh - Git-based deployment to Unraid server + +set -e + +# Configuration +UNRAID_HOST="${UNRAID_HOST:-root@192.168.2.61}" +REMOTE_PATH="/boot/config/plugins/compose.manager/projects/unraid-dash" +GIT_REPO="git@git.michaelsimard.ca:msimard/unraid-dashboard.git" + +echo "Deploying unraid-dash to ${UNRAID_HOST}..." + +ssh ${UNRAID_HOST} bash << EOF +set -e + +# Ensure SSH config for git server (port 28) +mkdir -p ~/.ssh +chmod 700 ~/.ssh +if ! grep -q "^Host git.michaelsimard.ca" ~/.ssh/config 2>/dev/null; then + echo "Configuring SSH for git.michaelsimard.ca (port 28)..." + cat >> ~/.ssh/config << 'SSHCONFIG' + +Host git.michaelsimard.ca + Port 28 +SSHCONFIG + chmod 600 ~/.ssh/config +fi + +# Clone or pull repository +if [ -d "${REMOTE_PATH}/.git" ]; then + echo "Repository exists, pulling latest changes..." + cd ${REMOTE_PATH} + git pull +else + echo "Cloning repository..." + git clone ${GIT_REPO} ${REMOTE_PATH} + cd ${REMOTE_PATH} +fi + +# Fix file permissions for Docker build (required for /boot partition) +echo "Fixing file permissions..." +chmod -R 755 ${REMOTE_PATH} +chmod 644 ${REMOTE_PATH}/*.html ${REMOTE_PATH}/*.css ${REMOTE_PATH}/*.js ${REMOTE_PATH}/*.yml ${REMOTE_PATH}/*.md 2>/dev/null || true + +# Check if config.js exists +if [ ! -f config.js ]; then + echo "WARNING: config.js file not found!" + echo "Please create ${REMOTE_PATH}/config.js with required configuration" + echo "You can copy from config.example.js and modify as needed" + exit 1 +fi + +# Build and deploy +echo "Building and starting container..." +docker compose up -d --build + +# Show status +echo "" +echo "Deployment complete!" +docker compose ps +echo "" +echo "View logs with: ssh ${UNRAID_HOST} 'cd ${REMOTE_PATH} && docker compose logs -f'" +EOF diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..351363a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + unraid-dash: + build: . + container_name: unraid-dash + restart: unless-stopped + ports: + - "9113:80" + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" diff --git a/index.html b/index.html index d25de46..12dc77e 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,7 @@ +

THANOS SYSTEMS MONITOR

@@ -19,7 +20,7 @@
-
+
TOTAL DISK
@@ -28,7 +29,7 @@ 0 / 0 TB
-
+
DOCKER
@@ -39,7 +40,7 @@
-
+
PHOTOS
@@ -48,7 +49,7 @@ 0 / 0 TB
-
+
MEDIA
diff --git a/manage.sh b/manage.sh new file mode 100755 index 0000000..157f78b --- /dev/null +++ b/manage.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# manage.sh - Manage unraid-dash on Unraid + +UNRAID_HOST="${UNRAID_HOST:-root@192.168.2.61}" +REMOTE_PATH="/boot/config/plugins/compose.manager/projects/unraid-dash" + +CMD="$1" + +run_remote() { + ssh ${UNRAID_HOST} "cd ${REMOTE_PATH} && $1" +} + +case "$CMD" in + logs) + run_remote "docker compose logs -f" + ;; + status) + run_remote "docker compose ps" + ;; + restart) + run_remote "docker compose restart" + echo "Dashboard restarted" + ;; + stop) + run_remote "docker compose down" + echo "Dashboard stopped" + ;; + start) + run_remote "docker compose up -d" + echo "Dashboard started" + ;; + rebuild) + run_remote "docker compose up -d --build" + echo "Dashboard rebuilt and started" + ;; + shell) + ssh -t ${UNRAID_HOST} "cd ${REMOTE_PATH} && bash" + ;; + *) + echo "Usage: $0 {logs|status|restart|stop|start|rebuild|shell}" + echo "" + echo "Commands:" + echo " logs - View live logs" + echo " status - Show container status" + echo " restart - Restart the dashboard" + echo " stop - Stop the dashboard" + echo " start - Start the dashboard" + echo " rebuild - Rebuild and restart the dashboard" + echo " shell - SSH into Unraid at dashboard directory" + exit 1 + ;; +esac diff --git a/script.js b/script.js index e303189..be7fddd 100644 --- a/script.js +++ b/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 `${icons[trend]}`; +} + +// 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 `:${port}`; +} + +// Helper function to create tooltip HTML +function createTooltip(title, data) { + const rows = Object.entries(data).map(([label, value]) => + `
+ ${label}: + ${value} +
` + ).join(''); + + return `
+
${title}
+
+ ${rows} +
+
`; +} + +// 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 = `
${diskLabel} @@ -291,8 +444,9 @@ function updateDisks(disks) {
- ${usedPercentage}% USED + ${usedPercentage}% USED ${createTrendIndicator(trend)}
+ ${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 = ` -
${docker.name}
+
${docker.name}${portLink}
${docker.status.toUpperCase()}
`; @@ -449,7 +609,8 @@ async function updateDashboard() { return { name: name, - status: state + status: state, + ports: container.ports || [] }; }); console.log('Processed docker data:', dockerData); diff --git a/setup-unraid.sh b/setup-unraid.sh new file mode 100755 index 0000000..6910b99 --- /dev/null +++ b/setup-unraid.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# setup-unraid.sh - Initial Unraid setup for git-based deployment + +set -e + +UNRAID_HOST="${UNRAID_HOST:-root@192.168.2.61}" +REMOTE_PATH="/boot/config/plugins/compose.manager/projects/unraid-dash" + +echo "Setting up Unraid server for git-based deployment..." +echo "" + +# Setup SSH config on Unraid for git access +echo "1. Configuring SSH for git.michaelsimard.ca..." +ssh ${UNRAID_HOST} bash << 'EOF' +mkdir -p ~/.ssh +chmod 700 ~/.ssh + +# Add SSH config for git server +if ! grep -q "git.michaelsimard.ca" ~/.ssh/config 2>/dev/null; then + cat >> ~/.ssh/config << 'SSHCONFIG' + +Host git.michaelsimard.ca + Port 28 +SSHCONFIG + chmod 600 ~/.ssh/config + echo "SSH config updated" +else + echo "SSH config already contains git.michaelsimard.ca" +fi + +# Add host key +ssh-keyscan -p 28 git.michaelsimard.ca >> ~/.ssh/known_hosts 2>/dev/null || true +echo "Host key added to known_hosts" +EOF + +echo "" +echo "2. Checking for SSH key on Unraid..." +ssh ${UNRAID_HOST} bash << 'EOF' +if [ ! -f ~/.ssh/id_rsa ] && [ ! -f ~/.ssh/id_ed25519 ]; then + echo "No SSH key found. Generating ed25519 key..." + ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -C "unraid@unraid-dash" + echo "" + echo "=== PUBLIC KEY - Add this to your Gitea account ===" + cat ~/.ssh/id_ed25519.pub + echo "=== END PUBLIC KEY ===" + echo "" + echo "Add this key at: https://git.michaelsimard.ca/user/settings/keys" + echo "Press Enter when done..." + read +else + echo "SSH key exists" + if [ -f ~/.ssh/id_ed25519.pub ]; then + cat ~/.ssh/id_ed25519.pub + elif [ -f ~/.ssh/id_rsa.pub ]; then + cat ~/.ssh/id_rsa.pub + fi +fi +EOF + +echo "" +echo "3. Testing git access..." +if ssh ${UNRAID_HOST} "git ls-remote git@git.michaelsimard.ca:msimard/unraid-dashboard.git HEAD" &>/dev/null; then + echo "Git access successful" +else + echo "Git access failed. Ensure SSH key is added to Gitea" + exit 1 +fi + +echo "" +echo "4. Creating config.js file on Unraid..." +if ssh ${UNRAID_HOST} "[ -f ${REMOTE_PATH}/config.js ]"; then + echo "config.js already exists on Unraid" +else + echo "Copying local config.js to Unraid..." + ssh ${UNRAID_HOST} "mkdir -p ${REMOTE_PATH}" + scp config.js ${UNRAID_HOST}:${REMOTE_PATH}/config.js + echo "config.js copied" +fi + +echo "" +echo "=== Setup Complete ===" +echo "Run './deploy.sh' to deploy the dashboard" diff --git a/styles.css b/styles.css index a252bd8..b37dba4 100644 --- a/styles.css +++ b/styles.css @@ -4,6 +4,45 @@ box-sizing: border-box; } +/* CSS Variables for theming */ +:root { + --bg-primary: #000000; + --bg-secondary: #0a0a0a; + --bg-tertiary: #1a1a1a; + --color-primary: #00ffff; + --color-secondary: #00cccc; + --color-text: #00ffff; + --color-warning: #ff00ff; + --color-critical: #ffffff; + --border-color: #00ffff; + --shadow-color: rgba(0, 255, 255, 0.3); + --shadow-color-intense: rgba(0, 255, 255, 0.6); + --panel-hover-gradient: linear-gradient(45deg, #00ffff, #ff00ff, #00ffff); + --progress-gradient: linear-gradient(90deg, #00ffff, #00cccc); + --progress-warning: linear-gradient(90deg, #ff00ff, #ff00cc); + --progress-critical: linear-gradient(90deg, #ffffff, #cccccc); + --scanline-color: #00ffff; +} + +body.light-mode { + --bg-primary: #ffffff; + --bg-secondary: #f0f5fa; + --bg-tertiary: #d9e6f2; + --color-primary: #0052a3; + --color-secondary: #003d7a; + --color-text: #002952; + --color-warning: #d9534f; + --color-critical: #c9302c; + --border-color: #0066cc; + --shadow-color: rgba(0, 82, 163, 0.2); + --shadow-color-intense: rgba(0, 82, 163, 0.4); + --panel-hover-gradient: linear-gradient(45deg, #0066cc, #d9534f, #0066cc); + --progress-gradient: linear-gradient(90deg, #0066cc, #0052a3); + --progress-warning: linear-gradient(90deg, #f0ad4e, #ec971f); + --progress-critical: linear-gradient(90deg, #d9534f, #c9302c); + --scanline-color: #0066cc; +} + @keyframes borderGlow { 0%, 100% { box-shadow: 0 0 15px rgba(0, 255, 255, 0.3), 0 0 30px rgba(0, 255, 255, 0.1); @@ -31,14 +70,35 @@ } } +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.6; + } +} + body { font-family: 'Courier New', monospace; - background-color: #000000; - color: #00ffff; + background-color: var(--bg-primary); + color: var(--color-text); min-height: 100vh; padding: 20px; position: relative; overflow-x: hidden; + transition: background-color 0.3s ease, color 0.3s ease; } body::before { @@ -48,7 +108,7 @@ body::before { left: 0; width: 100%; height: 3px; - background: linear-gradient(90deg, transparent, #00ffff, transparent); + background: linear-gradient(90deg, transparent, var(--scanline-color), transparent); animation: scanline 4s linear infinite; opacity: 0.3; pointer-events: none; @@ -64,20 +124,24 @@ header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; - border-bottom: 2px solid #00ffff; + border-bottom: 2px solid var(--border-color); } .title { font-size: 2.5rem; letter-spacing: 4px; - text-shadow: 0 0 10px #00ffff, 0 0 20px #00ffff; + color: var(--color-primary); margin-bottom: 10px; +} + +body:not(.light-mode) .title { + text-shadow: 0 0 10px var(--color-primary), 0 0 20px var(--color-primary); animation: titleGlow 3s ease-in-out infinite; } .timestamp { font-size: 0.9rem; - color: #00cccc; + color: var(--color-secondary); letter-spacing: 2px; } @@ -96,12 +160,16 @@ header { } .panel { - background-color: #0a0a0a; - border: 2px solid #00ffff; + background-color: var(--bg-secondary); + border: 2px solid var(--border-color); padding: 20px; - box-shadow: 0 0 15px rgba(0, 255, 255, 0.3); - animation: borderGlow 2s ease-in-out infinite; + box-shadow: 0 0 15px var(--shadow-color); position: relative; + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +body:not(.light-mode) .panel { + animation: borderGlow 2s ease-in-out infinite; } .panel::before { @@ -111,7 +179,7 @@ header { left: -2px; right: -2px; bottom: -2px; - background: linear-gradient(45deg, #00ffff, #ff00ff, #00ffff); + background: var(--panel-hover-gradient); z-index: -1; opacity: 0; transition: opacity 0.3s ease; @@ -131,8 +199,12 @@ header { letter-spacing: 3px; margin-bottom: 20px; padding-bottom: 10px; - border-bottom: 1px solid #00ffff; - text-shadow: 0 0 5px #00ffff; + border-bottom: 1px solid var(--border-color); + color: var(--color-primary); +} + +body:not(.light-mode) .panel-title { + text-shadow: 0 0 5px var(--color-primary); } /* Resource Section */ @@ -188,6 +260,14 @@ header { .resource-item { margin-bottom: 20px; + padding: 10px; + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +body.light-mode .resource-item { + border: 1px solid var(--border-color); + background-color: var(--bg-secondary); + padding: 15px; } .resource-label { @@ -221,27 +301,27 @@ header { .ring-text { font-size: 2rem; font-weight: bold; - fill: #00ffff; + fill: var(--color-primary); font-family: 'Courier New', monospace; } .ring-text-small { font-size: 1.5rem; font-weight: bold; - fill: #00ffff; + fill: var(--color-primary); font-family: 'Courier New', monospace; } .ring-fraction { font-size: 0.7rem; - fill: #00ffff; + fill: var(--color-primary); font-family: 'Courier New', monospace; opacity: 1; } .ring-fraction-small { font-size: 0.5rem; - fill: #00ffff; + fill: var(--color-primary); font-family: 'Courier New', monospace; opacity: 1; } @@ -250,16 +330,17 @@ header { position: relative; width: 100%; height: 20px; - background-color: #1a1a1a; - border: 1px solid #00ffff; + background-color: var(--bg-tertiary); + border: 1px solid var(--border-color); overflow: hidden; + transition: background-color 0.3s ease, border-color 0.3s ease; } .progress-fill { height: 100%; - background: linear-gradient(90deg, #00ffff, #00cccc); + background: var(--progress-gradient); transition: width 0.3s ease, background 0.3s ease; - box-shadow: 0 0 10px rgba(0, 255, 255, 0.5); + box-shadow: 0 0 10px var(--shadow-color); position: relative; overflow: hidden; } @@ -285,13 +366,13 @@ header { } .progress-fill.warning { - background: linear-gradient(90deg, #ff00ff, #ff00cc); - box-shadow: 0 0 10px rgba(255, 0, 255, 0.5); + background: var(--progress-warning); + box-shadow: 0 0 10px var(--shadow-color-intense); } .progress-fill.critical { - background: linear-gradient(90deg, #ffffff, #cccccc); - box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); + background: var(--progress-critical); + box-shadow: 0 0 10px var(--shadow-color-intense); } .progress-text { @@ -300,8 +381,17 @@ header { left: 50%; transform: translate(-50%, -50%); font-weight: bold; - text-shadow: 1px 1px 2px #000000; font-size: 0.75rem; + color: #ffffff; +} + +body.light-mode .progress-text { + color: #ffffff; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); +} + +body:not(.light-mode) .progress-text { + text-shadow: 1px 1px 2px #000000; } /* Parity Status */ @@ -314,8 +404,17 @@ header { display: flex; justify-content: space-between; padding: 10px; - background-color: #1a1a1a; - border-left: 3px solid #00ffff; + background-color: var(--bg-tertiary); + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +body:not(.light-mode) .status-item { + border-left: 3px solid var(--border-color); +} + +body.light-mode .status-item { + border: 1px solid var(--border-color); + background-color: var(--bg-secondary); } .status-label { @@ -324,20 +423,32 @@ header { } .status-value { - color: #00ffff; + color: var(--color-primary); } .status-value.error { - color: #ffffff; - text-shadow: 0 0 5px #ffffff; + color: var(--color-critical); +} + +body:not(.light-mode) .status-value.error { + text-shadow: 0 0 5px var(--color-critical); } /* Disk Array */ .disk-item { margin-bottom: 8px; padding: 8px; - background-color: #1a1a1a; - border-left: 3px solid #00ffff; + background-color: var(--bg-tertiary); + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +body:not(.light-mode) .disk-item { + border-left: 3px solid var(--border-color); +} + +body.light-mode .disk-item { + border: 1px solid var(--border-color); + background-color: var(--bg-secondary); } .disk-header { @@ -353,7 +464,7 @@ header { } .disk-info { - color: #00cccc; + color: var(--color-secondary); } /* Docker Containers */ @@ -365,9 +476,10 @@ header { .docker-item { padding: 15px; - background-color: #1a1a1a; - border: 1px solid #00ffff; + background-color: var(--bg-tertiary); + border: 1px solid var(--border-color); text-align: center; + transition: background-color 0.3s ease, border-color 0.3s ease; } .docker-name { @@ -375,6 +487,10 @@ header { margin-bottom: 10px; letter-spacing: 1px; font-size: 0.9rem; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; } .docker-status { @@ -386,8 +502,15 @@ header { .docker-status.running { background-color: #00ffff; - color: #000000; + color: #003d7a; box-shadow: 0 0 5px #00ffff; + font-weight: bold; +} + +body.light-mode .docker-status.running { + background-color: #00cc99; + color: #ffffff; + box-shadow: 0 0 5px rgba(0, 204, 153, 0.5); } .docker-status.stopped, @@ -397,12 +520,214 @@ header { box-shadow: 0 0 5px #666666; } +body.light-mode .docker-status.stopped, +body.light-mode .docker-status.offline { + background-color: #6c757d; + color: #ffffff; + box-shadow: 0 0 5px rgba(108, 117, 125, 0.5); +} + .docker-status.paused { background-color: #ff00ff; - color: #000000; + color: #003d7a; box-shadow: 0 0 5px #ff00ff; } +body.light-mode .docker-status.paused { + background-color: #f0ad4e; + color: #ffffff; + box-shadow: 0 0 5px rgba(240, 173, 78, 0.5); +} + +.webui-link { + color: var(--color-primary); + text-decoration: none; + font-size: 0.85rem; + padding: 2px 6px; + border: 1px solid var(--border-color); + border-radius: 3px; + transition: all 0.3s ease; + display: inline-block; +} + +.webui-link:hover { + background-color: var(--color-primary); + color: var(--bg-primary); + box-shadow: 0 0 10px var(--shadow-color-intense); + text-shadow: none; + transform: scale(1.05); +} + +/* Tooltips */ +.tooltip-container { + position: relative; + display: inline-block; +} + +.tooltip { + visibility: hidden; + opacity: 0; + position: absolute; + z-index: 1000; + background-color: var(--bg-secondary); + border: 2px solid var(--border-color); + padding: 10px; + border-radius: 4px; + box-shadow: 0 0 20px var(--shadow-color-intense); + min-width: 200px; + transition: opacity 0.3s ease, visibility 0.3s ease, background-color 0.3s ease; + pointer-events: none; + animation: fadeIn 0.3s ease; +} + +.tooltip-container:hover .tooltip { + visibility: visible; + opacity: 1; +} + +.tooltip-title { + font-weight: bold; + color: var(--color-primary); + margin-bottom: 8px; + border-bottom: 1px solid var(--border-color); + padding-bottom: 5px; +} + +.tooltip-content { + color: var(--color-secondary); + font-size: 0.85rem; + line-height: 1.5; +} + +.tooltip-row { + display: flex; + justify-content: space-between; + margin-bottom: 4px; +} + +.tooltip-label { + color: var(--color-primary); +} + +.tooltip-value { + color: var(--color-text); + font-weight: bold; +} + +/* Trend Indicators */ +.trend-indicator { + display: inline-block; + margin-left: 8px; + font-size: 0.8rem; + animation: pulse 2s ease-in-out infinite; +} + +.trend-up { + color: var(--color-warning); +} + +.trend-down { + color: var(--color-primary); +} + +.trend-stable { + color: var(--color-secondary); + opacity: 0.5; +} + +/* Enhanced Ring Chart Container */ +.mini-ring-chart-container { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + cursor: pointer; + transition: transform 0.2s ease; +} + +.mini-ring-chart-container:hover { + transform: scale(1.05); +} + +.mini-ring-chart-container .tooltip { + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 10px; +} + +/* Enhanced text transitions */ +.ring-text, .ring-text-small { + transition: fill 0.3s ease; +} + +.ring-fraction, .ring-fraction-small { + transition: fill 0.3s ease; +} + +/* Disk item enhancements */ +.disk-item { + transition: background-color 0.3s ease, border-left-color 0.3s ease; + cursor: pointer; +} + +body:not(.light-mode) .disk-item:hover { + background-color: #252525; + border-left-color: #ff00ff; +} + +body.light-mode .disk-item:hover { + background-color: #d0d0d0; + border-left-color: var(--color-warning); +} + +/* Light mode text overrides to ensure no black text */ +body.light-mode .disk-name, +body.light-mode .docker-name, +body.light-mode .status-label, +body.light-mode .resource-label { + color: var(--color-text); +} + +body.light-mode .disk-info { + color: var(--color-secondary); +} + +.disk-item .tooltip { + top: 0; + left: 100%; + margin-left: 10px; +} + +/* Theme Toggle Button */ +.theme-toggle { + position: fixed; + top: 20px; + right: 20px; + background-color: var(--bg-secondary); + border: 2px solid var(--border-color); + color: var(--color-primary); + padding: 10px 20px; + font-family: 'Courier New', monospace; + font-size: 0.9rem; + cursor: pointer; + letter-spacing: 2px; + transition: all 0.3s ease; + box-shadow: 0 0 10px var(--shadow-color); + z-index: 1000; +} + +.theme-toggle:hover { + background-color: var(--color-primary); + color: var(--bg-primary); + box-shadow: 0 0 20px var(--shadow-color-intense); + transform: scale(1.05); +} + +body:not(.light-mode) .theme-toggle:hover { + text-shadow: none; +} + /* Responsive Design */ @media (max-width: 768px) { .title { @@ -417,4 +742,9 @@ header { .docker-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } + + .tooltip { + min-width: 150px; + font-size: 0.75rem; + } }