Add selfh.st icon replacement script for Docker and VM icons

Server-side bash script reads `glass.icon` Docker labels and replaces
Unraid's cached icon PNGs with selfh.st CDN versions. Auto-detects
icon names from container names when no label is set. Supports VMs
via vm-icons.conf mapping file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kaloyan Danchev
2026-02-08 11:29:56 +02:00
parent 969c240553
commit 673366db5f
3 changed files with 313 additions and 0 deletions

View File

@@ -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`.

266
update-icons.sh Normal file
View File

@@ -0,0 +1,266 @@
#!/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 "netdisco"; return 0 ;;
netdisco-web) echo "netdisco"; 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 "urbackup"; return 0 ;;
netalertx) echo "netalertx"; 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
View 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