- Add JS icon replacement module to sidebar.js that swaps container icons with selfh.st CDN versions on Docker page and Dashboard via MutationObserver - Fix Dashboard container name extraction (Strategy 2: span.inner > span) for containers whose original icon was missing (question.png fallback) - Add CSS filter for Dashboard folder icons (folder-img-docker class) - Add icon mappings for containers without CDN matches (netdisco→networking-toolbox, urbackup→networking-toolbox, seekandwatch→databasement, unmarr→homarr, dockhand→docker) - Fix shadowed netdisco-web/netdisco-backend entries in update-icons.sh - Add Community Applications (Apps page) glass theme overrides Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
270 lines
8.2 KiB
Bash
270 lines
8.2 KiB
Bash
#!/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"
|