Compare commits
9 Commits
c4b11fce6b
...
fbacc229ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbacc229ea | ||
|
|
c9932f275a | ||
|
|
596e828223 | ||
|
|
673366db5f | ||
|
|
969c240553 | ||
|
|
b17efa805f | ||
|
|
01fcb31052 | ||
|
|
fb07089ee5 | ||
|
|
494f689008 |
36
CLAUDE.md
36
CLAUDE.md
@@ -12,6 +12,8 @@ Custom CSS + JS theme for Unraid 7.2.3 WebGUI. B&W mountain wallpaper with frost
|
||||
|------|---------|--------------------------|---------------------|
|
||||
| `style.css` | Main CSS theme | `/boot/config/plugins/custom.css/style.css` | `/usr/local/emhttp/plugins/custom.css/style.css` |
|
||||
| `sidebar.js` | Sidebar toggle + search overlay | `/boot/config/plugins/custom.css/assets/sidebar.js` | `/usr/local/emhttp/plugins/custom.css/assets/sidebar.js` |
|
||||
| `update-icons.sh` | Replace Docker/VM icons with selfh.st CDN icons | `/boot/config/plugins/custom.css/assets/update-icons.sh` | `/usr/local/emhttp/plugins/custom.css/assets/update-icons.sh` |
|
||||
| `vm-icons.conf` | VM name → icon name mapping | `/boot/config/plugins/custom.css/assets/vm-icons.conf` | — |
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -21,6 +23,9 @@ scp -i ~/.ssh/id_ed25519_unraid -P 422 style.css root@192.168.10.20:/boot/config
|
||||
scp -i ~/.ssh/id_ed25519_unraid -P 422 style.css root@192.168.10.20:/usr/local/emhttp/plugins/custom.css/style.css
|
||||
scp -i ~/.ssh/id_ed25519_unraid -P 422 sidebar.js root@192.168.10.20:/boot/config/plugins/custom.css/assets/sidebar.js
|
||||
scp -i ~/.ssh/id_ed25519_unraid -P 422 sidebar.js root@192.168.10.20:/usr/local/emhttp/plugins/custom.css/assets/sidebar.js
|
||||
scp -i ~/.ssh/id_ed25519_unraid -P 422 update-icons.sh root@192.168.10.20:/boot/config/plugins/custom.css/assets/update-icons.sh
|
||||
scp -i ~/.ssh/id_ed25519_unraid -P 422 update-icons.sh root@192.168.10.20:/usr/local/emhttp/plugins/custom.css/assets/update-icons.sh
|
||||
scp -i ~/.ssh/id_ed25519_unraid -P 422 vm-icons.conf root@192.168.10.20:/boot/config/plugins/custom.css/assets/vm-icons.conf
|
||||
```
|
||||
|
||||
JS injection is handled by `CustomJS_Loader.page` on the server (persisted via `/boot/config/go`).
|
||||
@@ -106,6 +111,37 @@ The `dynamix.gui.search` plugin uses hover-trigger in sidebar theme. We replace
|
||||
- Cmd+K / Ctrl+K keyboard shortcut
|
||||
- Escape to close
|
||||
|
||||
## Custom Docker/VM Icons
|
||||
|
||||
Icons are sourced from [selfh.st/icons](https://selfh.st/icons/) via jsDelivr CDN.
|
||||
|
||||
### How it works
|
||||
`update-icons.sh` reads a `glass.icon` Docker label from each container, downloads the matching PNG from `https://cdn.jsdelivr.net/gh/selfhst/icons@main/png/{name}.png`, and replaces Unraid's cached icon files. This works on Docker page, VM page, and Dashboard — no client-side JS needed.
|
||||
|
||||
### Docker label convention
|
||||
Add label `glass.icon` to containers:
|
||||
- Short name: `glass.icon=plex` → fetches from selfh.st CDN
|
||||
- Full URL: `glass.icon=https://example.com/icon.png` → fetches from URL
|
||||
- No label: auto-detects from container name (lowercase + CDN HEAD check)
|
||||
|
||||
### VM icons
|
||||
Edit `vm-icons.conf` with `VM_NAME=icon_name` entries (one per line).
|
||||
|
||||
### Running
|
||||
```bash
|
||||
# Dry run (show what would change)
|
||||
ssh -i ~/.ssh/id_ed25519_unraid root@192.168.10.20 -p 422 'bash /boot/config/plugins/custom.css/assets/update-icons.sh --dry-run'
|
||||
# Real run
|
||||
ssh -i ~/.ssh/id_ed25519_unraid root@192.168.10.20 -p 422 'bash /boot/config/plugins/custom.css/assets/update-icons.sh'
|
||||
# Force re-download all
|
||||
ssh -i ~/.ssh/id_ed25519_unraid root@192.168.10.20 -p 422 'bash /boot/config/plugins/custom.css/assets/update-icons.sh --force'
|
||||
```
|
||||
|
||||
### Icon paths on Unraid
|
||||
- RAM cache: `/usr/local/emhttp/state/plugins/dynamix.docker.manager/images/{Name}-icon.png`
|
||||
- Persistent: `/var/lib/docker/unraid/images/{Name}-icon.png`
|
||||
- VM templates: `/usr/local/emhttp/plugins/dynamix.vm.manager/templates/images/`
|
||||
|
||||
## Wallpaper
|
||||
|
||||
Place wallpaper at `/boot/config/plugins/custom.css/assets/wallpaper.jpg` — accessible at `/custom/wallpaper.jpg`.
|
||||
|
||||
194
sidebar.js
194
sidebar.js
@@ -54,6 +54,10 @@
|
||||
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();
|
||||
@@ -141,3 +145,193 @@
|
||||
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 <img> 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 });
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
function initIcons() {
|
||||
// Initial replacement
|
||||
setTimeout(replaceIcons, 500);
|
||||
// Second pass after FolderView finishes its DOM work
|
||||
setTimeout(replaceIcons, 2000);
|
||||
// Start watching for future changes
|
||||
startObserver();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initIcons);
|
||||
} else {
|
||||
initIcons();
|
||||
}
|
||||
})();
|
||||
|
||||
269
update-icons.sh
Normal file
269
update-icons.sh
Normal file
@@ -0,0 +1,269 @@
|
||||
#!/bin/bash
|
||||
# update-icons.sh — Replace Unraid Docker/VM icons with selfh.st CDN icons
|
||||
# Part of the unraid-glass theme project
|
||||
set -euo pipefail
|
||||
|
||||
# --- Configuration ---
|
||||
CDN_BASE="https://cdn.jsdelivr.net/gh/selfhst/icons@main/png"
|
||||
ICON_RAM="/usr/local/emhttp/state/plugins/dynamix.docker.manager/images"
|
||||
ICON_PERSIST="/var/lib/docker/unraid/images"
|
||||
VM_ICON_DIR="/usr/local/emhttp/plugins/dynamix.vm.manager/templates/images"
|
||||
CONFIG_DIR="/boot/config/plugins/custom.css/assets"
|
||||
VM_CONFIG="${CONFIG_DIR}/vm-icons.conf"
|
||||
TMP_DIR="/tmp/glass-icons"
|
||||
LABEL_NAME="glass.icon"
|
||||
|
||||
FORCE=false
|
||||
QUIET=false
|
||||
DRY_RUN=false
|
||||
DOCKER_ONLY=false
|
||||
VM_ONLY=false
|
||||
|
||||
updated=0
|
||||
skipped=0
|
||||
failed=0
|
||||
|
||||
# --- Helpers ---
|
||||
log() { $QUIET || echo "$@"; }
|
||||
warn() { echo "WARN: $@" >&2; }
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: update-icons.sh [OPTIONS]
|
||||
|
||||
Replace Unraid Docker/VM icons with selfh.st CDN icons.
|
||||
|
||||
Options:
|
||||
--force Re-download even if icon file already matches
|
||||
--quiet Suppress output (for cron)
|
||||
--dry-run Show what would be done without doing it
|
||||
--docker-only Skip VM icon processing
|
||||
--vm-only Skip Docker icon processing
|
||||
--help Show this help
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Argument parsing ---
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--force) FORCE=true ;;
|
||||
--quiet) QUIET=true ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
--docker-only) DOCKER_ONLY=true ;;
|
||||
--vm-only) VM_ONLY=true ;;
|
||||
--help) usage ;;
|
||||
*) warn "Unknown option: $1"; usage ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# --- Auto-detection: try to match container name to selfh.st icon ---
|
||||
cdn_exists() {
|
||||
curl -fsSL --head --max-time 5 "${CDN_BASE}/${1}.png" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
auto_detect_icon() {
|
||||
local container="$1"
|
||||
local name
|
||||
|
||||
# Known mappings for containers that won't auto-match
|
||||
case "$(echo "$container" | tr '[:upper:]' '[:lower:]')" in
|
||||
autokuma) echo "uptime-kuma"; return 0 ;;
|
||||
uptimekuma) echo "uptime-kuma"; return 0 ;;
|
||||
uptime-kuma-api) echo "uptime-kuma"; return 0 ;;
|
||||
homeassistant_inabox) echo "home-assistant"; return 0 ;;
|
||||
homeassistant*) echo "home-assistant"; return 0 ;;
|
||||
dockersocket) echo "docker"; return 0 ;;
|
||||
pgadmin*) echo "pgadmin"; return 0 ;;
|
||||
postgresql*) echo "postgresql"; return 0 ;;
|
||||
woodpecker-agent) echo "woodpecker-ci"; return 0 ;;
|
||||
woodpecker-server) echo "woodpecker-ci"; return 0 ;;
|
||||
rustdesk-hbbr) echo "rustdesk"; return 0 ;;
|
||||
rustdesk-hbbs) echo "rustdesk"; return 0 ;;
|
||||
rustfs) echo "rustdesk"; return 0 ;;
|
||||
netbox-redis-cache) echo "redis"; return 0 ;;
|
||||
netbox-worker) echo "netbox"; return 0 ;;
|
||||
netdisco-backend) echo "networking-toolbox"; return 0 ;;
|
||||
netdisco-web) echo "networking-toolbox"; return 0 ;;
|
||||
libation) echo "audible"; return 0 ;;
|
||||
diode-*) echo "netbox"; return 0 ;;
|
||||
authentik-worker) echo "authentik"; return 0 ;;
|
||||
adguardhome-sync) echo "adguard-home"; return 0 ;;
|
||||
adguardhome) echo "adguard-home"; return 0 ;;
|
||||
actual-budget) echo "actual-budget"; return 0 ;;
|
||||
speedtest-tracker) echo "speedtest-tracker"; return 0 ;;
|
||||
timemachine) echo "apple"; return 0 ;;
|
||||
open-webui) echo "open-webui"; return 0 ;;
|
||||
urbackup) echo "networking-toolbox"; return 0 ;;
|
||||
netalertx) echo "netalertx"; return 0 ;;
|
||||
seekandwatch) echo "databasement"; return 0 ;;
|
||||
unmarr) echo "homarr"; return 0 ;;
|
||||
dockhand) echo "docker"; return 0 ;;
|
||||
esac
|
||||
|
||||
# Strategy 1: lowercase name directly
|
||||
name=$(echo "$container" | tr '[:upper:]' '[:lower:]')
|
||||
if cdn_exists "$name"; then echo "$name"; return 0; fi
|
||||
|
||||
# Strategy 2: underscores to hyphens
|
||||
name=$(echo "$container" | tr '[:upper:]' '[:lower:]' | tr '_' '-')
|
||||
if cdn_exists "$name"; then echo "$name"; return 0; fi
|
||||
|
||||
# Strategy 3: strip common suffixes
|
||||
name=$(echo "$container" | tr '[:upper:]' '[:lower:]' | sed -E 's/-(worker|agent|server|web|backend|api|cache|migrate|bootstrap|sync|ingress|ingester|reconciler)$//')
|
||||
if cdn_exists "$name"; then echo "$name"; return 0; fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Resolve label value to a full URL
|
||||
resolve_url() {
|
||||
local value="$1"
|
||||
if [[ "$value" == http* ]]; then
|
||||
echo "$value"
|
||||
else
|
||||
echo "${CDN_BASE}/${value}.png"
|
||||
fi
|
||||
}
|
||||
|
||||
# Download icon to temp, validate, and copy to target paths
|
||||
download_and_replace() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local target_ram="$3"
|
||||
local target_persist="$4"
|
||||
local tmp_file="${TMP_DIR}/${name}.png"
|
||||
|
||||
if $DRY_RUN; then
|
||||
log " [DRY-RUN] Would download: $url"
|
||||
log " [DRY-RUN] Would replace: $target_ram"
|
||||
log " [DRY-RUN] Would replace: $target_persist"
|
||||
((updated++)) || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Download
|
||||
if ! curl -fsSL --max-time 15 -o "$tmp_file" "$url" 2>/dev/null; then
|
||||
warn "Download failed for $name: $url"
|
||||
((failed++)) || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Validate non-empty
|
||||
if [ ! -s "$tmp_file" ]; then
|
||||
warn "Empty download for $name: $url"
|
||||
rm -f "$tmp_file"
|
||||
((failed++)) || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Skip if identical (unless --force)
|
||||
if ! $FORCE && [ -f "$target_ram" ] && cmp -s "$tmp_file" "$target_ram"; then
|
||||
log " [SKIP] $name — icon unchanged"
|
||||
rm -f "$tmp_file"
|
||||
((skipped++)) || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Replace in both paths
|
||||
cp "$tmp_file" "$target_ram" 2>/dev/null || true
|
||||
if [ -n "$target_persist" ] && [ -d "$(dirname "$target_persist")" ]; then
|
||||
cp "$tmp_file" "$target_persist" 2>/dev/null || true
|
||||
fi
|
||||
rm -f "$tmp_file"
|
||||
|
||||
log " [OK] $name — icon updated"
|
||||
((updated++)) || true
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
mkdir -p "$TMP_DIR"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
# === Docker containers ===
|
||||
if ! $VM_ONLY; then
|
||||
log "=== Docker Containers ==="
|
||||
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
warn "Docker is not running, skipping containers"
|
||||
else
|
||||
while IFS= read -r container; do
|
||||
[ -z "$container" ] && continue
|
||||
log "Processing: $container"
|
||||
|
||||
# Read glass.icon label
|
||||
label=$(docker inspect --format "{{index .Config.Labels \"${LABEL_NAME}\"}}" "$container" 2>/dev/null || echo "")
|
||||
label="${label#<no value>}" # docker inspect returns "<no value>" if missing
|
||||
|
||||
if [ -n "$label" ]; then
|
||||
icon_url=$(resolve_url "$label")
|
||||
log " Label: $label → $icon_url"
|
||||
else
|
||||
# Auto-detect
|
||||
if icon_name=$(auto_detect_icon "$container"); then
|
||||
icon_url=$(resolve_url "$icon_name")
|
||||
log " Auto-detect: $icon_name → $icon_url"
|
||||
else
|
||||
log " [SKIP] No label, no auto-match"
|
||||
((skipped++)) || true
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
download_and_replace \
|
||||
"$container" \
|
||||
"$icon_url" \
|
||||
"${ICON_RAM}/${container}-icon.png" \
|
||||
"${ICON_PERSIST}/${container}-icon.png" || true
|
||||
|
||||
done < <(docker ps -a --format '{{.Names}}')
|
||||
fi
|
||||
fi
|
||||
|
||||
# === Virtual Machines ===
|
||||
if ! $DOCKER_ONLY; then
|
||||
log ""
|
||||
log "=== Virtual Machines ==="
|
||||
|
||||
if [ ! -f "$VM_CONFIG" ]; then
|
||||
log "No VM config found at $VM_CONFIG, skipping VMs"
|
||||
elif ! command -v virsh >/dev/null 2>&1; then
|
||||
warn "virsh not found, skipping VMs"
|
||||
else
|
||||
while IFS='=' read -r vm_name icon_value; do
|
||||
# Skip comments and empty lines
|
||||
[[ "$vm_name" =~ ^[[:space:]]*# ]] && continue
|
||||
[ -z "$vm_name" ] && continue
|
||||
vm_name=$(echo "$vm_name" | xargs) # trim whitespace
|
||||
icon_value=$(echo "$icon_value" | xargs)
|
||||
[ -z "$icon_value" ] && continue
|
||||
|
||||
log "Processing VM: $vm_name"
|
||||
|
||||
# Get current icon filename from VM XML
|
||||
icon_file=$(virsh dumpxml "$vm_name" 2>/dev/null | grep -oP 'icon="\K[^"]+' || echo "")
|
||||
if [ -z "$icon_file" ]; then
|
||||
warn "Could not read icon for VM '$vm_name'"
|
||||
((failed++)) || true
|
||||
continue
|
||||
fi
|
||||
|
||||
icon_url=$(resolve_url "$icon_value")
|
||||
log " Config: $icon_value → $icon_url"
|
||||
|
||||
download_and_replace \
|
||||
"vm-${vm_name// /-}" \
|
||||
"$icon_url" \
|
||||
"${VM_ICON_DIR}/${icon_file}" \
|
||||
"" || true
|
||||
|
||||
done < "$VM_CONFIG"
|
||||
fi
|
||||
fi
|
||||
|
||||
# === Summary ===
|
||||
log ""
|
||||
log "=== Done ==="
|
||||
log "Updated: $updated | Skipped: $skipped | Failed: $failed"
|
||||
11
vm-icons.conf
Normal file
11
vm-icons.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
# vm-icons.conf — VM icon overrides for unraid-glass theme
|
||||
# Format: VM_NAME=icon_name (or full URL)
|
||||
# VM_NAME must match exactly as shown in `virsh list --all`
|
||||
# Lines starting with # are comments, empty lines are ignored.
|
||||
#
|
||||
# Examples:
|
||||
# Home Assistant=home-assistant
|
||||
# Windows 11=windows-11
|
||||
# Ubuntu Server=ubuntu
|
||||
|
||||
Home Assistant=home-assistant
|
||||
Reference in New Issue
Block a user