diff --git a/CLAUDE.md b/CLAUDE.md index 71e11f4..ace31a5 100644 --- a/CLAUDE.md +++ b/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`. diff --git a/update-icons.sh b/update-icons.sh new file mode 100644 index 0000000..8489a30 --- /dev/null +++ b/update-icons.sh @@ -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#}" # docker inspect returns "" 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" diff --git a/vm-icons.conf b/vm-icons.conf new file mode 100644 index 0000000..5747f2c --- /dev/null +++ b/vm-icons.conf @@ -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