(function() { 'use strict'; var KEY = 'unraid-sidebar-expanded'; // Restore state immediately (before DOM ready to avoid flash) if (localStorage.getItem(KEY) === '1') { document.documentElement.classList.add('sidebar-expanded'); } function init() { var menu = document.getElementById('menu'); if (!menu) return; // --- Unraid logo (absolute positioned at very top of sidebar) --- if (!document.getElementById('sidebar-logo')) { var logo = document.createElement('img'); logo.id = 'sidebar-logo'; logo.src = 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/unraid.svg'; logo.alt = ''; menu.appendChild(logo); } // --- Toggle button with chevron (below logo) --- if (!document.getElementById('sidebar-toggle-btn')) { var btn = document.createElement('button'); btn.id = 'sidebar-toggle-btn'; btn.type = 'button'; btn.title = 'Toggle Sidebar'; btn.innerHTML = ''; btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); var expanded = document.documentElement.classList.toggle('sidebar-expanded'); localStorage.setItem(KEY, expanded ? '1' : '0'); }); menu.appendChild(btn); } // --- Lock button icon toggle (locked vs unlocked) --- initLockButton(); // --- Search overlay (delayed to wait for search plugin init) --- setTimeout(initSearchOverlay, 800); } function initLockButton() { var lockItem = document.querySelector('.nav-item.LockButton'); if (!lockItem) return; var lockLink = lockItem.querySelector('a'); if (!lockLink) return; // Set initial state based on title function updateLockState() { var title = lockLink.getAttribute('title') || ''; if (title.indexOf('Unlock') === 0) { lockItem.classList.add('is-locked'); } else { lockItem.classList.remove('is-locked'); } } updateLockState(); // Watch for title attribute changes (Unraid toggles this on click) var observer = new MutationObserver(function() { updateLockState(); }); observer.observe(lockLink, { attributes: true, attributeFilter: ['title'] }); } function initSearchOverlay() { if (!window.jQuery) return; var searchItem = document.querySelector('.nav-item.gui_search'); if (!searchItem) return; // Remove default hover-based open/close handlers $(searchItem).off('mouseenter mouseleave'); var link = searchItem.querySelector('a'); if (!link) return; // Remove any inline onclick handler (Unraid sets onclick="gui_search()") link.removeAttribute('onclick'); link.onclick = null; // Replace hover with click link.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); toggleSearch(); }); // Add Cmd+K / Ctrl+K keyboard shortcut (sidebar theme doesn't have one by default) document.addEventListener('keydown', function(e) { var isMac = navigator.platform.indexOf('Mac') > -1; if ((isMac ? e.metaKey : e.ctrlKey) && (e.key === 'k' || e.keyCode === 75)) { if ($('[role="modal"]:visible').length) return; e.preventDefault(); toggleSearch(); } }); } function toggleSearch() { var existing = document.getElementById('guiSearchBoxSpan'); if (existing) { closeSearch(); return; } // Call original gui_search to create the input if (typeof gui_search === 'function') { gui_search(); } // Move search box to — backdrop-filter on #menu creates a new // containing block, so position:fixed inside it is relative to #menu // and gets clipped by overflow:hidden. Moving to body fixes this. var searchSpan = document.getElementById('guiSearchBoxSpan'); if (searchSpan) { document.body.appendChild(searchSpan); } // Create backdrop overlay if (!document.getElementById('search-backdrop')) { var backdrop = document.createElement('div'); backdrop.id = 'search-backdrop'; backdrop.addEventListener('click', function() { closeSearch(); }); document.body.appendChild(backdrop); } // Override blur/keydown behavior on the input var input = document.getElementById('guiSearchBox'); if (input && window.jQuery) { $(input).off('blur keydown'); input.addEventListener('blur', function() { setTimeout(function() { var span = document.getElementById('guiSearchBoxSpan'); if (span && !span.contains(document.activeElement)) { closeSearch(); } }, 300); }); input.addEventListener('keydown', function(e) { if (e.key === 'Escape' || e.keyCode === 27) { e.preventDefault(); closeSearch(); } }); input.focus(); } } function closeSearch() { // Remove the search span directly (works whether moved to body or not) var span = document.getElementById('guiSearchBoxSpan'); if (span) span.remove(); var backdrop = document.getElementById('search-backdrop'); if (backdrop) backdrop.remove(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); // ============================================================= // Icon Replacement Module — selfh.st CDN icons for Docker/VMs // ============================================================= (function() { 'use strict'; var CDN_PNG = 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/png'; var CDN_SVG = 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg'; // Container name → selfh.st icon name (lowercase) // Only needed for names that don't auto-match. // Add entries here or use the glass.icon Docker label. var ICON_MAP = { 'autokuma': 'uptime-kuma', 'uptimekuma': 'uptime-kuma', 'uptime-kuma-api': 'uptime-kuma', 'homeassistant_inabox': 'home-assistant', 'dockersocket': 'docker', 'pgadmin4': 'pgadmin', 'postgresql17': 'postgresql', 'woodpecker-agent': 'woodpecker-ci', 'woodpecker-server': 'woodpecker-ci', 'rustdesk-hbbr': 'rustdesk', 'rustdesk-hbbs': 'rustdesk', 'rustfs': 'rustdesk', 'netbox-redis-cache': 'redis', 'netbox-worker': 'netbox', 'netdisco-backend': 'networking-toolbox', 'netdisco-web': 'networking-toolbox', 'diode-agent': 'netbox', 'diode-auth': 'netbox', 'diode-auth-bootstrap': 'netbox', 'diode-hydra': 'netbox', 'diode-hydra-migrate': 'netbox', 'diode-ingester': 'netbox', 'diode-ingress': 'netbox', 'diode-reconciler': 'netbox', 'authentik-worker': 'authentik', 'adguardhome': 'adguard-home', 'adguardhome-sync': 'adguard-home', 'timemachine': 'apple', 'libation': 'audible', 'netalertx': 'netalertx', 'actual-budget': 'actual-budget', 'speedtest-tracker': 'speedtest-tracker', 'open-webui': 'open-webui', 'urbackup': 'networking-toolbox', 'seekandwatch': 'databasement', 'unmarr': 'homarr', 'dockhand': 'docker' }; // Resolve container name to selfh.st CDN URL // mode: 'color' for expanded containers, 'light' for dashboard/folder-preview function resolveIconUrl(name, mode) { var key = name.toLowerCase(); var iconName = ICON_MAP[key] || key; if (mode === 'light') { return CDN_SVG + '/' + iconName + '-light.svg'; } return CDN_PNG + '/' + iconName + '.png'; } // Extract container name from an element by walking up the DOM function getContainerName(img) { // Strategy 1: sibling .inner > .appname text var outer = img.closest('span.outer'); if (outer) { var appname = outer.querySelector('span.appname'); if (appname) { var a = appname.querySelector('a'); return (a ? a.textContent : appname.textContent).trim(); } } // Strategy 2: .inner > first non-state span (Dashboard view) if (outer) { var inner = outer.querySelector('span.inner'); if (inner) { var nameSpan = inner.querySelector('span:not(.state)'); if (nameSpan && nameSpan.textContent.trim()) { return nameSpan.textContent.trim(); } } } // Strategy 3: .inner > a.exec text (FolderView uses this) if (outer) { var exec = outer.querySelector('a.exec'); if (exec) return exec.textContent.trim(); } // Strategy 4: parent row ct-name var row = img.closest('tr'); if (row) { var ctName = row.querySelector('td.ct-name span.appname'); if (ctName) { var a2 = ctName.querySelector('a'); return (a2 ? a2.textContent : ctName.textContent).trim(); } } // Strategy 4: extract from icon src path (fallback) var src = img.getAttribute('src') || ''; var match = src.match(/images\/(.+?)-icon\.png/); if (match) return match[1]; return null; } // Determine icon mode based on DOM context function getIconMode(img) { // Dashboard → light if (img.closest('table.dashboard')) return 'light'; // Folder preview (collapsed row) → light if (img.closest('div.folder-preview')) return 'light'; // Expanded container inside folder → color if (img.closest('tr.folder-element')) return 'color'; // Regular container row → color return 'color'; } // Replace all container icons on the page function replaceIcons() { var imgs = document.querySelectorAll('img.img'); imgs.forEach(function(img) { // Skip sidebar logo and non-container icons if (img.id === 'sidebar-logo') return; // Skip folder category icons (SVGs managed by FolderView) if (img.classList.contains('folder-img') && img.closest('td.folder-name')) return; if (img.classList.contains('folder-img-docker')) return; var name = getContainerName(img); if (!name) return; // Skip folder names (they start with "folder-") if (name.indexOf('folder-') === 0) return; var mode = getIconMode(img); var url = resolveIconUrl(name, mode); // Only replace if different (avoid infinite MutationObserver loop) if (img.src !== url && img.getAttribute('src') !== url) { img.src = url; // Remove cache-busting onerror that resets to question mark img.removeAttribute('onerror'); } }); } // Debounce to avoid excessive calls during DOM manipulation var replaceTimer = null; function debouncedReplace() { if (replaceTimer) clearTimeout(replaceTimer); replaceTimer = setTimeout(replaceIcons, 200); } // Watch for DOM changes (AJAX loads, FolderView manipulation) function startObserver() { var target = document.getElementById('displaybox') || document.body; var observer = new MutationObserver(function(mutations) { var dominated = false; for (var i = 0; i < mutations.length; i++) { if (mutations[i].addedNodes.length > 0) { dominated = true; break; } } if (dominated) debouncedReplace(); }); observer.observe(target, { childList: true, subtree: true }); } // Replace Tailscale icon on Dashboard and Settings pages function replaceTailscaleIcon() { var TAILSCALE_SVG = CDN_SVG + '/tailscale-light.svg'; // Dashboard: img with alt="Tailscale" inside a card header var imgs = document.querySelectorAll('img[alt="Tailscale"], img[src*="tailscale" i]'); imgs.forEach(function(img) { if (img.src !== TAILSCALE_SVG && img.getAttribute('src') !== TAILSCALE_SVG) { img.src = TAILSCALE_SVG; img.removeAttribute('onerror'); } }); } // Initialize when DOM is ready function initIcons() { // Initial replacement setTimeout(replaceIcons, 500); setTimeout(replaceTailscaleIcon, 500); // Second pass after FolderView finishes its DOM work setTimeout(replaceIcons, 2000); setTimeout(replaceTailscaleIcon, 2000); // Start watching for future changes startObserver(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initIcons); } else { initIcons(); } })(); // ============================================================= // Pie Chart Recoloring — vibrant theme colors for System card // ============================================================= (function() { 'use strict'; // Map Unraid's muted conic-gradient colors → vibrant theme colors var COLOR_MAP = [ // gray-blue used portions → theme accent [/rgb\(96,\s*110,\s*127\)/g, 'rgb(126, 184, 218)'], // near-black unused → subtle light [/rgb\(35,\s*37,\s*35\)/g, 'rgba(255, 255, 255, 0.08)'], // muted orange → vibrant orange [/rgb\(215,\s*126,\s*13\)/g, 'rgb(245, 166, 35)'], // muted yellow → vibrant yellow [/rgb\(212,\s*172,\s*13\)/g, 'rgb(241, 196, 15)'], // muted red → vibrant red-coral [/rgb\(205,\s*92,\s*92\)/g, 'rgb(231, 76, 60)'], ]; function recolorPie(pie) { var style = pie.getAttribute('style'); if (!style || style.indexOf('conic-gradient') === -1) return; var newStyle = style; for (var i = 0; i < COLOR_MAP.length; i++) { newStyle = newStyle.replace(COLOR_MAP[i][0], COLOR_MAP[i][1]); } if (newStyle !== style) { pie.setAttribute('style', newStyle); } } function recolorAll() { var pies = document.querySelectorAll('div.pie'); pies.forEach(recolorPie); } function startPieObserver() { var pies = document.querySelectorAll('div.pie'); if (pies.length === 0) return; var observer = new MutationObserver(function(mutations) { for (var i = 0; i < mutations.length; i++) { if (mutations[i].attributeName === 'style') { recolorPie(mutations[i].target); } } }); pies.forEach(function(pie) { recolorPie(pie); observer.observe(pie, { attributes: true, attributeFilter: ['style'] }); }); } function initPieCharts() { setTimeout(recolorAll, 1000); setTimeout(startPieObserver, 2000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initPieCharts); } else { initPieCharts(); } })(); // ============================================================= // Vertical Pill Bars — Array/Cache/Unassigned disk utilization // ============================================================= (function() { 'use strict'; function convertBar(bar) { // Skip bars with no data or already converted if (bar.classList.contains('none') || bar.classList.contains('vertical-bar')) return; var fill = bar.querySelector('span:first-child'); if (!fill) return; // Read width (e.g. "83%") and convert to height var w = fill.style.width; if (w) { fill.style.height = w; fill.style.width = '100%'; } // Copy % text from sibling .load span into bar's last span var parent = bar.parentElement; if (parent) { var loadSpan = parent.querySelector('.load'); var textSpan = bar.querySelector('span:last-child'); if (loadSpan && textSpan && textSpan !== fill) { textSpan.textContent = loadSpan.textContent.trim(); } } bar.classList.add('vertical-bar'); } function convertAllBars(container) { var bars = container.querySelectorAll('.usage-disk'); bars.forEach(convertBar); } function initVerticalBars() { var tbodyIds = ['array_list', 'pool_list0', 'devs_list']; var targets = []; tbodyIds.forEach(function(id) { var el = document.getElementById(id); if (el) { targets.push(el); convertAllBars(el); } }); if (targets.length === 0) return; // Watch for style changes (Unraid updates bar widths) and childList (dynamic adds) var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { var tbody = m.target.closest ? m.target.closest('tbody') : null; if (!tbody) tbody = m.target; // Reconvert: remove vertical-bar class from affected bars, then reconvert var bars = tbody.querySelectorAll('.usage-disk'); bars.forEach(function(bar) { if (bar.classList.contains('vertical-bar')) { bar.classList.remove('vertical-bar'); } convertBar(bar); }); }); }); targets.forEach(function(el) { observer.observe(el, { attributes: true, attributeFilter: ['style'], subtree: true, childList: true }); }); } function init() { setTimeout(initVerticalBars, 1500); // Second pass for late-loading content setTimeout(initVerticalBars, 3000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })(); // ============================================================= // System Card Legend Colors — fix invisible legend circles // ============================================================= (function() { 'use strict'; // Legend circle colors matching recolored pie segments (in DOM order) var LEGEND_COLORS = [ 'rgb(126, 184, 218)', // System — accent blue 'rgb(245, 166, 35)', // VM — orange 'rgb(241, 196, 15)', // ZFS cache — yellow 'rgb(231, 76, 60)', // Docker — red 'rgba(255, 255, 255, 0.3)' // Free — subtle white ]; function colorLegend() { var dynamic = document.getElementById('dynamic'); if (!dynamic) return; var circles = dynamic.querySelectorAll('i.fa-circle'); circles.forEach(function(circle, i) { if (i < LEGEND_COLORS.length) { circle.style.setProperty('color', LEGEND_COLORS[i], 'important'); } }); } function initLegendColors() { var dynamic = document.getElementById('dynamic'); if (!dynamic) return; colorLegend(); // Re-apply when Unraid re-renders the system card var observer = new MutationObserver(function() { colorLegend(); }); observer.observe(dynamic, { childList: true, subtree: true }); } function init() { setTimeout(initLegendColors, 2000); setTimeout(colorLegend, 4000); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();