#!/bin/bash # ProxMorph Theme Collection Installer for Proxmox VE, Proxmox Backup Server, and Proxmox Datacenter Manager # Supports: PVE 8.x/9.x, PBS 3.x/4.x, PDM 1.x # Integrates with native Proxmox theme selector set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' MAGENTA='\033[0;35m' NC='\033[0m' # No Color # Configuration VERSION="2.7.3" WIDGET_TOOLKIT_DIR="/usr/share/javascript/proxmox-widget-toolkit" THEMES_DIR="${WIDGET_TOOLKIT_DIR}/themes" PROXMOXLIB_JS="${WIDGET_TOOLKIT_DIR}/proxmoxlib.js" BACKUP_DIR="/root/.proxmorph-backup" GITEA_REPO="theraiwy/proxmorph" GITEA_HOST="gittheraiwy.theraiwy.top" RAW_BRANCH="main" INSTALL_DIR="/opt/proxmorph" # Sensor support paths SENSORS_CONFIG="${INSTALL_DIR}/.sensors-enabled" SENSORS_FILTER="${INSTALL_DIR}/.sensors-filter" NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" SENSORS_PATCH_MARKER="# ProxMorph Sensors" # PVE-specific paths PVE_MANAGER_DIR="/usr/share/pve-manager" PVE_INDEX_TPL="${PVE_MANAGER_DIR}/index.html.tpl" PVE_JS_PATCHES_DIR="${PVE_MANAGER_DIR}/js/proxmorph" PVE_SERVICE="pveproxy" # PBS-specific paths PBS_MANAGER_DIR="/usr/share/javascript/proxmox-backup" PBS_INDEX_HBS="${PBS_MANAGER_DIR}/index.hbs" PBS_JS_PATCHES_DIR="${PBS_MANAGER_DIR}/js/proxmorph" PBS_SERVICE="proxmox-backup-proxy" # PDM-specific paths (Proxmox Datacenter Manager) PDM_MANAGER_DIR="/usr/share/javascript/proxmox-datacenter-manager" PDM_INDEX_HBS="${PDM_MANAGER_DIR}/index.hbs" PDM_JS_PATCHES_DIR="${PDM_MANAGER_DIR}/js/proxmorph" PDM_THEMES_DIR="${PDM_MANAGER_DIR}/proxmorph-themes" PDM_SERVICE="proxmox-datacenter-api" # Product detection (set by check_product) PRODUCT="" PRODUCT_VERSION="" INDEX_TEMPLATE="" JS_PATCHES_DIR="" PROXY_SERVICE="" echo -e "${CYAN}" echo "╔═══════════════════════════════════════════════════════════╗" echo "║ ProxMorph Theme Collection for Proxmox VE, PBS & PDM ║" echo "╚═══════════════════════════════════════════════════════════╝" echo -e "${NC}" # Function to print colored messages print_status() { echo -e "${GREEN}[✓]${NC} $1"; } print_warning() { echo -e "${YELLOW}[!]${NC} $1"; } print_error() { echo -e "${RED}[✗]${NC} $1"; } print_info() { echo -e "${BLUE}[i]${NC} $1"; } print_theme() { echo -e "${MAGENTA}[T]${NC} $1"; } # Check if running as root check_root() { if [[ $EUID -ne 0 ]]; then print_error "This script must be run as root" exit 1 fi } # Check if Proxmox VE is installed check_pve() { if command -v pveversion &> /dev/null; then PRODUCT="PVE" PRODUCT_VERSION=$(pveversion | head -1) INDEX_TEMPLATE="$PVE_INDEX_TPL" JS_PATCHES_DIR="$PVE_JS_PATCHES_DIR" PROXY_SERVICE="$PVE_SERVICE" return 0 fi return 1 } # Check if Proxmox Backup Server is installed check_pbs() { if command -v proxmox-backup-manager &> /dev/null; then PRODUCT="PBS" PRODUCT_VERSION=$(proxmox-backup-manager version 2>/dev/null | head -1) INDEX_TEMPLATE="$PBS_INDEX_HBS" JS_PATCHES_DIR="$PBS_JS_PATCHES_DIR" PROXY_SERVICE="$PBS_SERVICE" return 0 fi return 1 } # Check if Proxmox Datacenter Manager is installed check_pdm() { if command -v proxmox-datacenter-manager &> /dev/null || \ dpkg -l proxmox-datacenter-manager-ui &> /dev/null; then PRODUCT="PDM" PRODUCT_VERSION=$(dpkg -l proxmox-datacenter-manager 2>/dev/null | awk '/^ii/{print "PDM " $3}' || echo "PDM (unknown version)") INDEX_TEMPLATE="$PDM_INDEX_HBS" JS_PATCHES_DIR="$PDM_JS_PATCHES_DIR" PROXY_SERVICE="$PDM_SERVICE" THEMES_DIR="$PDM_THEMES_DIR" return 0 fi return 1 } # Detect which Proxmox product is installed check_product() { if check_pve; then print_info "Detected: $PRODUCT_VERSION" elif check_pbs; then print_info "Detected: $PRODUCT_VERSION" elif check_pdm; then print_info "Detected: $PRODUCT_VERSION" else print_error "No supported Proxmox product detected." print_error "This script requires PVE 8.x/9.x, PBS 3.x/4.x, or PDM 1.x." exit 1 fi } # Get latest commit hash from Gitea get_latest_version() { curl -s "https://${GITEA_HOST}/api/v1/repos/${GITEA_REPO}/branches/${RAW_BRANCH}" | \ grep '"id"' | head -1 | sed -E 's/.*"id":"([^"]+)".*/\1/' | cut -c1-7 } # Download archive from Gitea download_release() { local version="${1:-$(get_latest_version)}" if [[ -z "$version" ]]; then print_error "Could not determine latest version" exit 1 fi print_info "Downloading ProxMorph (${version})..." local download_url="https://${GITEA_HOST}/${GITEA_REPO}/archive/${RAW_BRANCH}.tar.gz" local tmp_dir=$(mktemp -d) if ! curl -sL "$download_url" -o "${tmp_dir}/proxmorph.tar.gz"; then print_error "Failed to download archive" rm -rf "$tmp_dir" exit 1 fi # Extract to install directory mkdir -p "$INSTALL_DIR" rm -rf "${INSTALL_DIR:?}"/* tar -xzf "${tmp_dir}/proxmorph.tar.gz" -C "$tmp_dir" # Gitea archives extract to a subfolder named 'repo-branch' (e.g. proxmorph-main/) local extracted_dir=$(find "$tmp_dir" -maxdepth 1 -type d ! -path "$tmp_dir" | head -1) if [[ -n "$extracted_dir" ]]; then cp -r "${extracted_dir}/." "$INSTALL_DIR/" else print_error "Failed to extract archive" rm -rf "$tmp_dir" exit 1 fi rm -rf "$tmp_dir" # Save version info echo "$version" > "${INSTALL_DIR}/.version" print_status "Downloaded ProxMorph (${version})" } # Check for updates check_updates() { local current_version="" if [[ -f "${INSTALL_DIR}/.version" ]]; then current_version=$(cat "${INSTALL_DIR}/.version") fi local latest_version=$(get_latest_version) if [[ -z "$latest_version" ]]; then print_warning "Could not check for updates (no internet?)" return 1 fi if [[ "$current_version" == "$latest_version" ]]; then print_status "Already up to date (${current_version})" return 0 elif [[ -n "$current_version" ]]; then print_info "Update available: ${current_version} → ${latest_version}" return 2 else print_info "Latest commit: ${latest_version}" return 2 fi } # Create backup of original files backup_files() { mkdir -p "$BACKUP_DIR" if [[ "$PRODUCT" == "PDM" ]]; then # PDM: back up index.hbs only if [[ -f "$INDEX_TEMPLATE" && ! -f "${BACKUP_DIR}/index.hbs.original" ]]; then cp "$INDEX_TEMPLATE" "${BACKUP_DIR}/index.hbs.original" print_status "Created backup of index.hbs" fi elif [[ -f "$PROXMOXLIB_JS" && ! -f "${BACKUP_DIR}/proxmoxlib.js.original" ]]; then cp "$PROXMOXLIB_JS" "${BACKUP_DIR}/proxmoxlib.js.original" print_status "Created backup of proxmoxlib.js" fi } # Restore from package (clean state) restore_packages() { if [[ "$PRODUCT" == "PDM" ]]; then # PDM: restore index.hbs from backup if [[ -f "${BACKUP_DIR}/index.hbs.original" ]]; then cp "${BACKUP_DIR}/index.hbs.original" "$INDEX_TEMPLATE" print_status "Restored index.hbs from backup" else print_warning "No index.hbs backup found — manually reinstall proxmox-datacenter-manager-ui" fi return 0 fi print_info "Reinstalling widget toolkit to clean state..." apt-get -qq -o Dpkg::Use-Pty=0 reinstall proxmox-widget-toolkit 2>/dev/null print_status "Restored proxmox-widget-toolkit" } # Extract theme title from CSS file (first line comment) get_theme_title() { local css_file="$1" # First line should be: /*!Theme Name*/ local title=$(head -1 "$css_file" | sed -n 's|^/\*!\(.*\)\*/.*|\1|p') if [[ -z "$title" ]]; then # Fallback to filename title=$(basename "$css_file" .css | sed 's/theme-//' | sed 's/-/ /g' | sed 's/\b\(.\)/\u\1/g') fi echo "$title" } # Extract theme key from filename get_theme_key() { local css_file="$1" basename "$css_file" .css | sed 's/^theme-//' } # Add theme to theme_map in proxmoxlib.js patch_theme_map() { local theme_key="$1" local theme_title="$2" # Check if theme already exists if grep -q "\"${theme_key}\":" "$PROXMOXLIB_JS"; then print_info "Theme '${theme_key}' already registered" return 0 fi # Add theme to theme_map sed -i "s/theme_map: {/theme_map: {\n\t\"${theme_key}\": \"${theme_title}\",/" "$PROXMOXLIB_JS" if grep -q "\"${theme_key}\":" "$PROXMOXLIB_JS"; then print_theme "Registered: ${theme_title}" return 0 else print_error "Failed to register ${theme_title}" return 1 fi } # JavaScript Patches Configuration (Dynamic markers) JS_PATCH_MARKER="" JS_PATCH_MARKER_END="" # PDM CSS Theme Override Configuration (Dynamic markers) PDM_CSS_MARKER="" PDM_CSS_MARKER_END="" # Install JavaScript patches install_js_patches() { local patches_source="${1:-}" if [[ -z "$patches_source" ]]; then local themes_source=$(get_themes_source) if [[ -n "$themes_source" ]] && [[ -d "${themes_source}/patches" ]]; then patches_source="${themes_source}/patches" fi fi if [[ -z "$patches_source" ]] || [[ ! -d "$patches_source" ]]; then print_info "No JavaScript patches found (optional)" return 0 fi local js_count=$(find "$patches_source" -name "*.js" 2>/dev/null | wc -l) if [[ $js_count -eq 0 ]]; then print_info "No JavaScript patches to install" return 0 fi print_info "Installing $js_count JavaScript patch(es) for ${PRODUCT}..." # Create JS patches directory mkdir -p "$JS_PATCHES_DIR" # Copy JS files for js_file in "$patches_source"/*.js; do if [[ -f "$js_file" ]]; then cp "$js_file" "${JS_PATCHES_DIR}/" chmod 644 "${JS_PATCHES_DIR}/$(basename "$js_file")" print_theme "Installed: $(basename "$js_file")" fi done # Patch index template to load JS files if [[ -f "$INDEX_TEMPLATE" ]]; then # If already patched, remove old block so we re-generate with current file list if grep -q "$JS_PATCH_MARKER" "$INDEX_TEMPLATE"; then local escaped_start_jp=$(printf '%s\n' "$JS_PATCH_MARKER" | sed 's/[]\/$*.^[]/\\&/g') local escaped_end_jp=$(printf '%s\n' "$JS_PATCH_MARKER_END" | sed 's/[]\/$*.^[]/\\&/g') sed -i "/${escaped_start_jp}/,/${escaped_end_jp}/d" "$INDEX_TEMPLATE" print_info "Refreshing JS patch list in $(basename "$INDEX_TEMPLATE")" fi { local script_tags="$JS_PATCH_MARKER" local js_web_path="" if [[ "$PRODUCT" == "PVE" ]]; then js_web_path="/pve2/js/proxmorph" elif [[ "$PRODUCT" == "PDM" ]]; then js_web_path="/pdm/js/proxmorph" else js_web_path="/js/proxmorph" fi for js_file in "${JS_PATCHES_DIR}"/*.js; do if [[ -f "$js_file" ]]; then local js_name=$(basename "$js_file") script_tags="${script_tags}\n" fi done script_tags="${script_tags}\n${JS_PATCH_MARKER_END}" # Insert before sed -i "s||${script_tags}\n|" "$INDEX_TEMPLATE" print_status "Patched $(basename "$INDEX_TEMPLATE") with JS loader" } else print_warning "$(basename "$INDEX_TEMPLATE") not found - JS patches may not load" fi } # Remove JavaScript patches remove_js_patches() { if [[ -d "$JS_PATCHES_DIR" ]]; then rm -rf "$JS_PATCHES_DIR" print_info "Removed JS patches directory" fi if [[ -f "$INDEX_TEMPLATE" ]] && grep -q "$JS_PATCH_MARKER" "$INDEX_TEMPLATE"; then local escaped_start=$(printf '%s\n' "$JS_PATCH_MARKER" | sed 's/[]\/$*.^[]/\\&/g') local escaped_end=$(printf '%s\n' "$JS_PATCH_MARKER_END" | sed 's/[]\/$*.^[]/\\&/g') sed -i "\|${escaped_start}|,\|${escaped_end}|d" "$INDEX_TEMPLATE" print_info "Removed JS patch from $(basename "$INDEX_TEMPLATE")" fi } # Install PDM CSS theme overrides into index.hbs # PDM themes work by injecting a tag that overrides --pwt-color-* tokens # from the WASM-loaded base theme (Crisp/Desktop/Material) install_pdm_themes() { local themes_source="$1" print_info "Installing PDM theme overrides..." mkdir -p "$PDM_THEMES_DIR" mkdir -p "$PDM_JS_PATCHES_DIR" # Copy base component CSS (always-on styling: rounded corners, shadows, etc.) local base_css="${themes_source}/proxmorph-pdm-base.css" if [[ -f "$base_css" ]]; then cp "$base_css" "${PDM_THEMES_DIR}/" chmod 644 "${PDM_THEMES_DIR}/proxmorph-pdm-base.css" print_info "Installed PDM base component styles" fi local theme_count=0 for css_file in "$themes_source"/theme-*.css; do if [[ -f "$css_file" ]]; then cp "$css_file" "${PDM_THEMES_DIR}/" chmod 644 "${PDM_THEMES_DIR}/$(basename "$css_file")" local title=$(get_theme_title "$css_file") print_theme "Installed: ${title}" theme_count=$((theme_count + 1)) fi done if [[ $theme_count -eq 0 ]]; then print_warning "No PDM theme files found" return 1 fi # Copy theme selector JS patch # themes_source is either .../themes/pdm or /opt/proxmorph/themes/pdm # The JS patch lives in .../themes/patches/ (sibling to pdm/) local patches_dir="$(dirname "$themes_source")/patches" local selector_js="${patches_dir}/pdm-theme-selector.js" if [[ -f "$selector_js" ]]; then cp "$selector_js" "${PDM_JS_PATCHES_DIR}/" chmod 644 "${PDM_JS_PATCHES_DIR}/pdm-theme-selector.js" print_info "Installed PDM theme selector patch" fi # Inject CSS tags + JS into index.hbs if [[ -f "$INDEX_TEMPLATE" ]]; then # Remove old block if present if grep -q "$PDM_CSS_MARKER" "$INDEX_TEMPLATE"; then local esc_start=$(printf '%s\n' "$PDM_CSS_MARKER" | sed 's/[]\/$*.^[]/\\&/g') local esc_end=$(printf '%s\n' "$PDM_CSS_MARKER_END" | sed 's/[]\/$*.^[]/\\&/g') sed -i "\|${esc_start}|,\|${esc_end}|d" "$INDEX_TEMPLATE" print_info "Refreshing PDM theme links in $(basename "$INDEX_TEMPLATE")" fi # Build injection block as a temp file (avoids sed multiline issues) local tmpblock=$(mktemp) echo "$PDM_CSS_MARKER" > "$tmpblock" # Base CSS — always enabled (component styles) echo "" >> "$tmpblock" # Theme CSS links — disabled by default, activated by JS for css_file in "${PDM_THEMES_DIR}"/theme-*.css; do if [[ -f "$css_file" ]]; then local css_name=$(basename "$css_file") echo "" >> "$tmpblock" fi done # Inline activation script (runs before WASM loads) cat >> "$tmpblock" << 'JSBLOCK' JSBLOCK # Theme selector patch (injects themes into native PDM Theme dialog) echo "" >> "$tmpblock" echo "$PDM_CSS_MARKER_END" >> "$tmpblock" # Insert block before first using awk local tmpout=$(mktemp) awk -v blockfile="$tmpblock" 'BEGIN{done=0; while((getline line < blockfile)>0) block=block (block?"\n":"") line} !done && /<\/head>/{print block; done=1} {print}' "$INDEX_TEMPLATE" > "$tmpout" mv "$tmpout" "$INDEX_TEMPLATE" chmod 644 "$INDEX_TEMPLATE" rm -f "$tmpblock" print_status "Injected ${theme_count} theme(s) + base styles + selector patch into $(basename "$INDEX_TEMPLATE")" else print_warning "$(basename "$INDEX_TEMPLATE") not found — PDM themes may not load" fi print_status "PDM themes installed — ${theme_count} theme(s)" } # Remove PDM CSS theme overrides remove_pdm_themes() { if [[ -d "$PDM_THEMES_DIR" ]]; then rm -rf "$PDM_THEMES_DIR" print_info "Removed PDM theme overrides directory" fi if [[ -d "$PDM_JS_PATCHES_DIR" ]]; then rm -rf "$PDM_JS_PATCHES_DIR" print_info "Removed PDM JS patches directory" fi if [[ -f "$INDEX_TEMPLATE" ]] && grep -q "$PDM_CSS_MARKER" "$INDEX_TEMPLATE"; then local esc_start=$(printf '%s\n' "$PDM_CSS_MARKER" | sed 's/[]\/$*.^[]/\\&/g') local esc_end=$(printf '%s\n' "$PDM_CSS_MARKER_END" | sed 's/[]\/$*.^[]/\\&/g') sed -i "\|${esc_start}|,\|${esc_end}|d" "$INDEX_TEMPLATE" print_info "Removed PDM theme links from $(basename "$INDEX_TEMPLATE")" fi } # APT hook configuration for persistence across updates APT_HOOK_FILE="/etc/apt/apt.conf.d/99proxmorph" POST_INVOKE_SCRIPT="${INSTALL_DIR}/post-update.sh" # Install apt hook for automatic re-patching after updates install_apt_hook() { print_info "Installing apt hook for automatic re-patching..." mkdir -p "${INSTALL_DIR}" cat > "${POST_INVOKE_SCRIPT}" << SCRIPT #!/bin/bash # ProxMorph post-update hook - automatically re-patches after updates PRODUCT="${PRODUCT}" INSTALL_DIR="${INSTALL_DIR}" PROXMOXLIB_JS="${PROXMOXLIB_JS}" WIDGET_TOOLKIT_DIR="${WIDGET_TOOLKIT_DIR}" INDEX_TEMPLATE="${INDEX_TEMPLATE}" JS_PATCHES_DIR="${JS_PATCHES_DIR}" PROXY_SERVICE="${PROXY_SERVICE}" LOG_FILE="/var/log/proxmorph.log" JS_PATCH_MARKER="${JS_PATCH_MARKER}" JS_PATCH_MARKER_END="${JS_PATCH_MARKER_END}" PDM_CSS_MARKER="${PDM_CSS_MARKER}" PDM_CSS_MARKER_END="${PDM_CSS_MARKER_END}" PDM_THEMES_DIR="${PDM_THEMES_DIR}" # Set themes source based on product if [ "\$PRODUCT" = "PDM" ]; then THEMES_SOURCE="\${INSTALL_DIR}/themes/pdm" else THEMES_SOURCE="\${INSTALL_DIR}/themes" fi log() { echo "[\$(date '+%Y-%m-%d %H:%M:%S')] \$1" >> "\$LOG_FILE" } # Only proceed if themes are installed if [ ! -d "\$THEMES_SOURCE" ]; then exit 0 fi needs_repatch=false # PDM repatch check: see if CSS injection is still in index.hbs if [ "\$PRODUCT" = "PDM" ]; then if ! grep -q "\$PDM_CSS_MARKER" "\$INDEX_TEMPLATE" 2>/dev/null; then needs_repatch=true fi else # PVE/PBS: check if proxmoxlib.js needs patching for css_file in "\${THEMES_SOURCE}"/theme-*.css; do if [ -f "\$css_file" ]; then theme_key=\$(basename "\$css_file" .css | sed 's/^theme-//') if ! grep -q "\"\${theme_key}\":" "\$PROXMOXLIB_JS" 2>/dev/null; then needs_repatch=true fi break fi done fi # Check if template needs JS patch (PVE/PBS only) if [ "\$PRODUCT" != "PDM" ] && [ -d "\${THEMES_SOURCE}/patches" ] && ! grep -q "\$JS_PATCH_MARKER" "\$INDEX_TEMPLATE" 2>/dev/null; then needs_repatch=true fi if [ "\$needs_repatch" = "true" ]; then log "Detected \$PRODUCT update, re-applying ProxMorph patches..." if [ "\$PRODUCT" = "PDM" ]; then # PDM: Re-inject CSS overrides into index.hbs mkdir -p "\$PDM_THEMES_DIR" mkdir -p "\$PDM_JS_PATCHES_DIR" # Copy base component CSS base_css="\${THEMES_SOURCE}/proxmorph-pdm-base.css" if [ -f "\$base_css" ]; then cp "\$base_css" "\${PDM_THEMES_DIR}/" chmod 644 "\${PDM_THEMES_DIR}/proxmorph-pdm-base.css" fi for css_file in "\${THEMES_SOURCE}"/theme-*.css; do if [ -f "\$css_file" ]; then cp "\$css_file" "\${PDM_THEMES_DIR}/" chmod 644 "\${PDM_THEMES_DIR}/\$(basename "\$css_file")" log "Installed PDM theme: \$(basename "\$css_file")" fi done # Copy theme selector JS patch selector_js="\${INSTALL_DIR}/themes/patches/pdm-theme-selector.js" if [ -f "\$selector_js" ]; then cp "\$selector_js" "\${PDM_JS_PATCHES_DIR}/" chmod 644 "\${PDM_JS_PATCHES_DIR}/pdm-theme-selector.js" fi # Re-inject CSS link tags + JS if [ -f "\$INDEX_TEMPLATE" ] && ! grep -q "\$PDM_CSS_MARKER" "\$INDEX_TEMPLATE"; then tmpblock=\$(mktemp) echo "\$PDM_CSS_MARKER" > "\$tmpblock" # Base CSS (always enabled when theme active) echo "" >> "\$tmpblock" for css_file in "\${PDM_THEMES_DIR}"/theme-*.css; do if [ -f "\$css_file" ]; then css_name=\$(basename "\$css_file") echo "" >> "\$tmpblock" fi done cat >> "\$tmpblock" << 'JSBLK' JSBLK echo "" >> "\$tmpblock" echo "\$PDM_CSS_MARKER_END" >> "\$tmpblock" tmpout=\$(mktemp) awk -v blockfile="\$tmpblock" 'BEGIN{done=0; while((getline line < blockfile)>0) block=block (block?"\n":"") line} !done && /<\/head>/{print block; done=1} {print}' "\$INDEX_TEMPLATE" > "\$tmpout" mv "\$tmpout" "\$INDEX_TEMPLATE" chmod 644 "\$INDEX_TEMPLATE" rm -f "\$tmpblock" log "Re-injected PDM theme CSS into \$(basename "\$INDEX_TEMPLATE")" fi else # PVE/PBS: Re-register all themes in proxmoxlib.js for css_file in "\${THEMES_SOURCE}"/theme-*.css; do if [ -f "\$css_file" ]; then theme_key=\$(basename "\$css_file" .css | sed 's/^theme-//') theme_title=\$(head -1 "\$css_file" | sed -n 's|^/\*!\(.*\)\*/.*|\1|p') if [ -z "\$theme_title" ]; then theme_title=\$(echo "\$theme_key" | sed 's/-/ /g' | sed 's/\b\(.\)/\u\1/g') fi if ! grep -q "\"\${theme_key}\":" "\$PROXMOXLIB_JS"; then sed -i "s/theme_map: {/theme_map: {\n\t\"\${theme_key}\": \"\${theme_title}\",/" "\$PROXMOXLIB_JS" log "Registered theme: \${theme_title}" fi fi done # Re-apply JavaScript patches if [ -d "\${THEMES_SOURCE}/patches" ]; then mkdir -p "\$JS_PATCHES_DIR" for js_file in "\${THEMES_SOURCE}/patches"/*.js; do if [ -f "\$js_file" ]; then cp "\$js_file" "\$JS_PATCHES_DIR/" chmod 644 "\$JS_PATCHES_DIR/\$(basename "\$js_file")" log "Installed JS patch: \$(basename "\$js_file")" fi done # Patch template if needed if [ -f "\$INDEX_TEMPLATE" ] && ! grep -q "\$JS_PATCH_MARKER" "\$INDEX_TEMPLATE"; then script_tags="\$JS_PATCH_MARKER" js_web_path="" if [ "\$PRODUCT" = "PVE" ]; then js_web_path="/pve2/js/proxmorph" else js_web_path="/js/proxmorph" fi for js_file in "\$JS_PATCHES_DIR"/*.js; do if [ -f "\$js_file" ]; then js_name=\$(basename "\$js_file") script_tags="\${script_tags}\n" fi done script_tags="\${script_tags}\n\$JS_PATCH_MARKER_END" sed -i "s||\${script_tags}\n|" "\$INDEX_TEMPLATE" log "Patched \$(basename "\$INDEX_TEMPLATE") with JS loader" fi fi # Re-apply Nodes.pm sensor patch if enabled SENSORS_CONFIG="\${INSTALL_DIR}/.sensors-enabled" SENSORS_FILTER="\${INSTALL_DIR}/.sensors-filter" NODES_PM="/usr/share/perl5/PVE/API2/Nodes.pm" SENSORS_PATCH_MARKER="# ProxMorph Sensors" if [ -f "\$SENSORS_CONFIG" ] && [ -f "\$NODES_PM" ]; then sensor_needs_patch=false if ! grep -q "\$SENSORS_PATCH_MARKER" "\$NODES_PM" 2>/dev/null; then sensor_needs_patch=true elif ! grep -q "safe_ups_name" "\$NODES_PM" 2>/dev/null || ! grep -q "/usr/bin/timeout -k 1 3 /usr/bin/upsc" "\$NODES_PM" 2>/dev/null; then sed -i "/\${SENSORS_PATCH_MARKER}/,/\${SENSORS_PATCH_MARKER} END/d" "\$NODES_PM" sensor_needs_patch=true log "Detected legacy Nodes.pm sensor patch, refreshing" fi if [ "\$sensor_needs_patch" = "true" ]; then sed -i "/^[[:space:]]*my \\\$dinfo = df/i\\\\ \${SENSORS_PATCH_MARKER}\\\\ local \\\$ENV{PATH} = '/usr/bin:/bin';\\\\ \\\$res->{sensorsOutput} = \\\`sensors -j 2>/dev/null\\\`;\\\\ if (-e '\${SENSORS_FILTER}') {\\\\ if (open(my \\\$fh, '<', '\${SENSORS_FILTER}')) {\\\\ local \\\$/;\\\\ \\\$res->{sensorsFilter} = <\\\$fh>;\\\\ close(\\\$fh);\\\\ }\\\\ }\\\\ if (-x '/usr/bin/upsc') {\\\\ my \@ups_list = \\\`if [ -x /usr/bin/timeout ]; then /usr/bin/timeout -k 1 3 /usr/bin/upsc -l; else /usr/bin/upsc -l; fi 2>/dev/null\\\`;\\\\ if (\@ups_list) {\\\\ chomp(my \\\$ups_name = \\\$ups_list[0]);\\\\ if (\\\$ups_name && \\\$ups_name =~ /^([A-Za-z0-9_.:-]+)\\\$/) {\\\\ my \\\$safe_ups_name = \\\$1;\\\\ \\\$res->{upsData} = \\\`if [ -x /usr/bin/timeout ]; then /usr/bin/timeout -k 1 3 /usr/bin/upsc \\\$safe_ups_name; else /usr/bin/upsc \\\$safe_ups_name; fi 2>/dev/null\\\`;\\\\ }\\\\ }\\\\ }\\\\ \${SENSORS_PATCH_MARKER} END" "\$NODES_PM" if perl -c "\$NODES_PM" 2>/dev/null; then log "Re-patched Nodes.pm for sensor data" else log "ERROR: Nodes.pm syntax broken after sensor patch, rolling back" sed -i "/\${SENSORS_PATCH_MARKER}/,/\${SENSORS_PATCH_MARKER} END/d" "\$NODES_PM" fi fi fi fi # end PVE/PBS else branch # Restart proxy service to apply changes systemctl restart "\$PROXY_SERVICE" 2>/dev/null || true log "ProxMorph patches re-applied successfully" fi SCRIPT chmod +x "${POST_INVOKE_SCRIPT}" # Create apt hook cat > "${APT_HOOK_FILE}" << HOOK // ProxMorph: Automatically re-patch after updates DPkg::Post-Invoke { "if [ -x ${POST_INVOKE_SCRIPT} ]; then ${POST_INVOKE_SCRIPT}; fi"; }; HOOK print_status "Apt hook installed - themes will persist across ${PRODUCT} updates" } # Remove apt hook remove_apt_hook() { if [[ -f "${APT_HOOK_FILE}" ]]; then rm -f "${APT_HOOK_FILE}" print_info "Removed apt hook" fi if [[ -f "${POST_INVOKE_SCRIPT}" ]]; then rm -f "${POST_INVOKE_SCRIPT}" fi } # Check if apt hook is installed check_apt_hook() { if [[ -f "${APT_HOOK_FILE}" ]] && [[ -f "${POST_INVOKE_SCRIPT}" ]]; then return 0 fi return 1 } # ─── Hardware Sensor Support (PVE only) ───────────────────────── # Check if lm-sensors is available and what sensors exist detect_sensors() { if [[ "$PRODUCT" != "PVE" ]]; then print_warning "Hardware sensor support is only available for Proxmox VE" return 1 fi if ! command -v sensors &> /dev/null; then print_warning "lm-sensors is not installed" print_info "Install with: apt install lm-sensors && sensors-detect" return 1 fi local sensor_output sensor_output=$(sensors -j 2>/dev/null) || true if [[ -z "$sensor_output" ]]; then print_warning "No sensor data available. Run 'sensors-detect' first." return 1 fi # Report detected sensors local has_cpu=false has_nvme=false has_hdd=false has_fan=false has_ups=false echo "$sensor_output" | grep -q '"coretemp-isa-\|"k10temp-pci-' && has_cpu=true echo "$sensor_output" | grep -q '"nvme-pci-' && has_nvme=true echo "$sensor_output" | grep -q '"drivetemp-scsi-' && has_hdd=true echo "$sensor_output" | grep -q 'fan[0-9]*_input' && has_fan=true command -v upsc &> /dev/null && has_ups=true echo "" print_info "Detected hardware sensors:" [[ "$has_cpu" == "true" ]] && echo -e " ${GREEN}●${NC} CPU temperature (coretemp/k10temp)" [[ "$has_nvme" == "true" ]] && echo -e " ${GREEN}●${NC} NVMe drive temperature" [[ "$has_hdd" == "true" ]] && echo -e " ${GREEN}●${NC} HDD drive temperature (drivetemp)" [[ "$has_fan" == "true" ]] && echo -e " ${GREEN}●${NC} Fan speed" [[ "$has_ups" == "true" ]] && echo -e " ${GREEN}●${NC} UPS monitoring (NUT)" [[ "$has_cpu" == "false" && "$has_nvme" == "false" && "$has_hdd" == "false" && "$has_fan" == "false" ]] && { print_warning "No supported sensors found in lm-sensors output" return 1 } echo "" return 0 } # Enumerate individual sensors from sensors -j output for selection # Populates SENSOR_LIST array with entries like: # "CPU|coretemp-isa-0000|Package id 0|42.0°C" # "Fan|it8689-isa-0a40|fan1|850 RPM" enumerate_sensors() { SENSOR_LIST=() local sensor_output sensor_output=$(sensors -j 2>/dev/null) || true if [[ -z "$sensor_output" ]]; then return 1 fi # Parse JSON with awk to extract chip keys, labels, and values # CPU chips (coretemp, k10temp) local cpu_chips cpu_chips=$(echo "$sensor_output" | grep -oP '"(coretemp-isa-[^"]+|k10temp-pci-[^"]+)"' | tr -d '"' | sort -u) for chip in $cpu_chips; do # Get package/Tctl temp local temp temp=$(echo "$sensor_output" | python3 -c " import sys,json try: d=json.loads(sys.stdin.read()) chip=d.get('$chip',{}) for lbl,v in chip.items(): if lbl=='Adapter': continue if not isinstance(v,dict): continue for k,val in v.items(): if 'input' in k and val is not None: print(f'{lbl}|{val}') break except: pass " 2>/dev/null) if [[ -n "$temp" ]]; then while IFS='|' read -r label val; do SENSOR_LIST+=("CPU|${chip}|${label}|${val}°C") done <<< "$temp" fi done # NVMe drives local nvme_chips nvme_chips=$(echo "$sensor_output" | grep -oP '"(nvme-pci-[^"]+)"' | tr -d '"' | sort -u) for chip in $nvme_chips; do local temp temp=$(echo "$sensor_output" | python3 -c " import sys,json try: d=json.loads(sys.stdin.read()) chip=d.get('$chip',{}) for lbl,v in chip.items(): if lbl=='Adapter': continue if not isinstance(v,dict): continue for k,val in v.items(): if 'input' in k and val is not None: print(f'{lbl}|{val}') break except: pass " 2>/dev/null) if [[ -n "$temp" ]]; then while IFS='|' read -r label val; do SENSOR_LIST+=("NVMe|${chip}|${label}|${val}°C") done <<< "$temp" fi done # HDD/SATA drives local hdd_chips hdd_chips=$(echo "$sensor_output" | grep -oP '"(drivetemp-scsi-[^"]+)"' | tr -d '"' | sort -u) for chip in $hdd_chips; do local temp temp=$(echo "$sensor_output" | python3 -c " import sys,json try: d=json.loads(sys.stdin.read()) chip=d.get('$chip',{}) for lbl,v in chip.items(): if lbl=='Adapter': continue if not isinstance(v,dict): continue for k,val in v.items(): if 'input' in k and val is not None: print(f'{lbl}|{val}') break except: pass " 2>/dev/null) if [[ -n "$temp" ]]; then while IFS='|' read -r label val; do SENSOR_LIST+=("HDD|${chip}|${label}|${val}°C") done <<< "$temp" fi done # Fan sensors (any chip) local fan_data fan_data=$(echo "$sensor_output" | python3 -c " import sys,json try: d=json.loads(sys.stdin.read()) def find_fans(obj, chip_key, parent=''): if not isinstance(obj, dict): return for k,v in obj.items(): if k == 'Adapter': continue if isinstance(v, dict): for fk,fv in v.items(): if 'fan' in fk and 'input' in fk and fv is not None: print(f'{chip_key}|{k}|{int(fv)} RPM') if not any('fan' in fk and 'input' in fk for fk in v): find_fans(v, chip_key, k) for chip_key in d: find_fans(d[chip_key], chip_key) except: pass " 2>/dev/null) if [[ -n "$fan_data" ]]; then while IFS='|' read -r chip label val; do SENSOR_LIST+=("Fan|${chip}|${label}|${val}") done <<< "$fan_data" fi # UPS if command -v upsc &> /dev/null; then local ups_list ups_list=$(upsc -l 2>/dev/null | head -5) if [[ -n "$ups_list" ]]; then while read -r ups_name; do [[ -z "$ups_name" ]] && continue SENSOR_LIST+=("UPS|ups|${ups_name}|NUT") done <<< "$ups_list" fi fi return 0 } # Interactive sensor selection — lets user pick which sensors to display configure_sensor_filter() { if ! enumerate_sensors; then print_warning "Could not enumerate sensors" return 1 fi if [[ ${#SENSOR_LIST[@]} -eq 0 ]]; then print_warning "No individual sensors found to configure" return 1 fi echo "" echo "=== Sensor Selection ===" echo "" print_info "Available sensors:" echo "" local i=1 for entry in "${SENSOR_LIST[@]}"; do IFS='|' read -r type chip label value <<< "$entry" printf " ${CYAN}%2d)${NC} [%-4s] %s: %s (%s)\n" "$i" "$type" "$chip" "$label" "$value" i=$((i + 1)) done echo "" echo -e " ${CYAN} a)${NC} All sensors (default)" echo "" read -p "Select sensors to display [1-$((i-1)), comma-separated, or 'a' for all]: " selection # Handle 'all' or empty if [[ -z "$selection" || "$selection" == "a" || "$selection" == "all" ]]; then rm -f "$SENSORS_FILTER" print_status "All sensors will be displayed" return 0 fi # Parse comma-separated numbers local filter_entries=() IFS=',' read -ra nums <<< "$selection" for num in "${nums[@]}"; do num=$(echo "$num" | tr -d ' ') if [[ "$num" =~ ^[0-9]+$ ]] && [[ "$num" -ge 1 ]] && [[ "$num" -le ${#SENSOR_LIST[@]} ]]; then local entry="${SENSOR_LIST[$((num - 1))]}" IFS='|' read -r type chip label value <<< "$entry" # Store as chip:label for fans/temps, or chip:upsname for UPS filter_entries+=("${chip}:${label}") else print_warning "Ignoring invalid selection: $num" fi done if [[ ${#filter_entries[@]} -eq 0 ]]; then print_warning "No valid selections — showing all sensors" rm -f "$SENSORS_FILTER" return 0 fi # Write filter file mkdir -p "$INSTALL_DIR" printf '%s\n' "${filter_entries[@]}" > "$SENSORS_FILTER" print_status "Sensor filter saved (${#filter_entries[@]} sensor(s) selected)" return 0 } # Patch Nodes.pm to expose sensor data via the API patch_nodes_pm() { if [[ ! -f "$NODES_PM" ]]; then print_error "Nodes.pm not found at $NODES_PM" return 1 fi # Pre-flight: verify Nodes.pm is syntactically valid before we touch it. # If it is already broken (e.g. from the v2.7.0 APT-hook heredoc bug, #45) # the post-patch perl -c check would produce a misleading error. if ! perl -c "$NODES_PM" 2>/dev/null; then print_error "Nodes.pm already has a syntax error before patching." print_error "Run 'apt install --reinstall pve-manager' to restore it, then re-run ProxMorph install." return 1 fi # Refresh legacy sensor patches so old installs get the taint-safe logic. if grep -q "$SENSORS_PATCH_MARKER" "$NODES_PM" 2>/dev/null; then if grep -q "local \$ENV{PATH} = '/usr/bin:/bin';" "$NODES_PM" 2>/dev/null && \ grep -q "safe_ups_name" "$NODES_PM" 2>/dev/null && \ grep -q "/usr/bin/timeout -k 1 3 /usr/bin/upsc" "$NODES_PM" 2>/dev/null; then print_info "Nodes.pm already patched for sensors" return 0 fi print_warning "Legacy sensor patch detected in Nodes.pm, refreshing to current version" sed -i "/${SENSORS_PATCH_MARKER}/,/${SENSORS_PATCH_MARKER} END/d" "$NODES_PM" fi # Backup Nodes.pm if [[ ! -f "${BACKUP_DIR}/Nodes.pm.original" ]]; then mkdir -p "$BACKUP_DIR" cp "$NODES_PM" "${BACKUP_DIR}/Nodes.pm.original" print_info "Backed up Nodes.pm" fi # Insert sensor data collection before 'my $dinfo = df('/', 1);' sed -i "/^\s*my \$dinfo = df/i\\ ${SENSORS_PATCH_MARKER}\\ local \$ENV{PATH} = '/usr/bin:/bin';\\ \$res->{sensorsOutput} = \`sensors -j 2>/dev/null\`;\\ if (-e '${SENSORS_FILTER}') {\\ if (open(my \$fh, '<', '${SENSORS_FILTER}')) {\\ local \$/;\\ \$res->{sensorsFilter} = <\$fh>;\\ close(\$fh);\\ }\\ }\\ if (-x '/usr/bin/upsc') {\\ my \@ups_list = \`if [ -x /usr/bin/timeout ]; then /usr/bin/timeout -k 1 3 /usr/bin/upsc -l; else /usr/bin/upsc -l; fi 2>/dev/null\`;\\ if (\@ups_list) {\\ chomp(my \$ups_name = \$ups_list[0]);\\ if (\$ups_name && \$ups_name =~ /^([A-Za-z0-9_.:-]+)$/) {\\ my \$safe_ups_name = \$1;\\ \$res->{upsData} = \`if [ -x /usr/bin/timeout ]; then /usr/bin/timeout -k 1 3 /usr/bin/upsc \$safe_ups_name; else /usr/bin/upsc \$safe_ups_name; fi 2>/dev/null\`;\\ }\\ }\\ }\\ ${SENSORS_PATCH_MARKER} END" "$NODES_PM" if ! perl -c "$NODES_PM" 2>/dev/null; then print_error "Nodes.pm syntax broken after sensor patch, rolling back" if [[ -f "${BACKUP_DIR}/Nodes.pm.original" ]]; then cp "${BACKUP_DIR}/Nodes.pm.original" "$NODES_PM" print_info "Restored Nodes.pm from backup" else sed -i "/${SENSORS_PATCH_MARKER}/,/${SENSORS_PATCH_MARKER} END/d" "$NODES_PM" fi return 1 fi print_status "Patched Nodes.pm to expose hardware sensor data" return 0 } # Remove sensor patches from Nodes.pm unpatch_nodes_pm() { if [[ ! -f "$NODES_PM" ]]; then return 0 fi if ! grep -q "$SENSORS_PATCH_MARKER" "$NODES_PM" 2>/dev/null; then return 0 fi # Remove lines between our markers (inclusive) sed -i "/${SENSORS_PATCH_MARKER}/,/${SENSORS_PATCH_MARKER} END/d" "$NODES_PM" print_status "Removed sensor patches from Nodes.pm" } # Get list of remote cluster node hostnames (excludes local node) get_remote_nodes() { if ! command -v pvecm &>/dev/null; then return 0 fi # pvecm nodes can include a Qdevice status column (e.g. NR, NA,NV,NMW). # Use the last field as node name, skip local and qdevice rows. pvecm nodes 2>/dev/null | awk '$1 ~ /^[0-9]+$/ && $3 != "Qdevice" && $NF != "(local)" {print $NF}' } # Deploy sensor API patch to remote cluster nodes via scp patch_cluster_sensors() { local remote_nodes remote_nodes=$(get_remote_nodes) if [[ -z "$remote_nodes" ]]; then return 0 fi # Get local PVE version for comparison local local_version local_version=$(dpkg -l pve-manager 2>/dev/null | awk '/^ii/{print $3}') echo "" print_info "Cluster detected. Remote nodes need the API patch for sensors to work." echo "" for node in $remote_nodes; do echo -e " ${CYAN}●${NC} ${node}" done echo "" read -p "Deploy sensor patch to remote nodes? [Y/n]: " deploy_choice case "$deploy_choice" in [Nn]|[Nn][Oo]) print_info "Skipping remote nodes. Run 'install.sh manage-sensors enable' on each node individually." return 0 ;; esac for node in $remote_nodes; do print_info "Deploying to ${node}..." # Verify same PVE version before copying Nodes.pm local remote_version remote_version=$(ssh -o ConnectTimeout=5 "root@${node}" "dpkg -l pve-manager 2>/dev/null | awk '/^ii/{print \$3}'" 2>/dev/null) if [[ -n "$local_version" && -n "$remote_version" && "$local_version" != "$remote_version" ]]; then print_warning "Version mismatch on ${node} (local: ${local_version}, remote: ${remote_version}) — skipping" print_info "Run install.sh on ${node} directly to enable sensors" continue fi if scp -o ConnectTimeout=5 -q "$NODES_PM" "root@${node}:${NODES_PM}" 2>/dev/null; then # Sync sensor filter file if it exists if [[ -f "$SENSORS_FILTER" ]]; then scp -o ConnectTimeout=5 -q "$SENSORS_FILTER" "root@${node}:${SENSORS_FILTER}" 2>/dev/null || true fi if ssh -o ConnectTimeout=5 "root@${node}" "systemctl restart pveproxy" 2>/dev/null; then print_status "Sensors deployed to ${node}" else print_warning "Patched ${node} but failed to restart pveproxy" fi else print_warning "Failed to deploy to ${node} — run install.sh on that node directly" fi done } # Remove sensor API patch from remote cluster nodes unpatch_cluster_sensors() { local remote_nodes remote_nodes=$(get_remote_nodes) if [[ -z "$remote_nodes" ]]; then return 0 fi for node in $remote_nodes; do print_info "Removing sensor patch from ${node}..." if ssh -o ConnectTimeout=5 "root@${node}" \ "sed -i '/# ProxMorph Sensors/,/# ProxMorph Sensors END/d' /usr/share/perl5/PVE/API2/Nodes.pm 2>/dev/null && systemctl restart pveproxy" 2>/dev/null; then print_status "Sensors removed from ${node}" else print_warning "Failed to unpatch ${node}" fi done } # Install sensor support interactively install_sensors() { if [[ "$PRODUCT" != "PVE" ]]; then return 0 fi if ! detect_sensors; then return 0 fi read -p "Enable hardware sensor monitoring in the dashboard? [y/N]: " sensor_choice case "$sensor_choice" in [Yy]|[Yy][Ee][Ss]) patch_nodes_pm || return 1 mkdir -p "$INSTALL_DIR" echo "enabled" > "$SENSORS_CONFIG" print_status "Hardware sensor monitoring enabled!" echo "" read -p "Would you like to choose which sensors to display? [y/N]: " filter_choice case "$filter_choice" in [Yy]|[Yy][Ee][Ss]) configure_sensor_filter ;; *) print_info "Showing all sensors (can be configured later with: install.sh manage-sensors)" ;; esac patch_cluster_sensors ;; *) print_info "Skipping sensor integration (can be enabled later with: install.sh sensors enable)" ;; esac } # Remove sensor support remove_sensors() { unpatch_nodes_pm unpatch_cluster_sensors if [[ -f "$SENSORS_CONFIG" ]]; then rm -f "$SENSORS_CONFIG" print_info "Sensor configuration removed" fi if [[ -f "$SENSORS_FILTER" ]]; then rm -f "$SENSORS_FILTER" print_info "Sensor filter removed" fi } # Check if sensors are enabled check_sensors() { if [[ -f "$SENSORS_CONFIG" ]] && grep -q "$SENSORS_PATCH_MARKER" "$NODES_PM" 2>/dev/null; then return 0 fi return 1 } # Manage sensors subcommand manage_sensors() { local action="${1:-status}" case "$action" in enable) if [[ "$PRODUCT" != "PVE" ]]; then print_error "Sensor support is only available for Proxmox VE" exit 1 fi detect_sensors || exit 1 patch_nodes_pm || exit 1 mkdir -p "$INSTALL_DIR" echo "enabled" > "$SENSORS_CONFIG" print_status "Hardware sensor monitoring enabled!" print_info "Restarting ${PROXY_SERVICE}..." nohup systemctl restart "${PROXY_SERVICE}" &>/dev/null & patch_cluster_sensors ;; disable) remove_sensors print_status "Hardware sensor monitoring disabled" print_info "Restarting ${PROXY_SERVICE}..." nohup systemctl restart "${PROXY_SERVICE}" &>/dev/null & ;; detect) detect_sensors ;; configure) if [[ "$PRODUCT" != "PVE" ]]; then print_error "Sensor support is only available for Proxmox VE" exit 1 fi if ! check_sensors; then print_error "Sensors are not enabled. Enable them first with: install.sh manage-sensors enable" exit 1 fi configure_sensor_filter # Re-patch Nodes.pm so the filter file path is current unpatch_nodes_pm patch_nodes_pm print_info "Restarting ${PROXY_SERVICE}..." systemctl restart "${PROXY_SERVICE}" print_status "Sensor filter applied!" ;; status|*) if check_sensors; then echo -e " Sensors: ${GREEN}Enabled${NC}" if [[ -f "$SENSORS_FILTER" ]]; then local count count=$(wc -l < "$SENSORS_FILTER" 2>/dev/null) echo -e " Filter: ${CYAN}${count} sensor(s) selected${NC}" else echo -e " Filter: ${CYAN}All sensors (no filter)${NC}" fi else echo -e " Sensors: ${YELLOW}Disabled${NC}" fi ;; esac } # Interactive sensors management menu (called from show_menu option 7) manage_sensors_menu() { if [[ "$PRODUCT" != "PVE" ]]; then print_error "Sensor support is only available for Proxmox VE" return fi echo "" echo "=== Hardware Sensor Management ===" echo "" manage_sensors status echo "" echo " 1) Enable sensors" echo " 2) Disable sensors" echo " 3) Detect available sensors" echo " 4) Configure sensor selection" echo " 0) Back to main menu" echo "" read -p "Enter choice [0-4]: " sensor_choice case $sensor_choice in 1) manage_sensors enable ;; 2) manage_sensors disable ;; 3) manage_sensors detect ;; 4) manage_sensors configure ;; 0) show_menu ;; *) print_error "Invalid option" ; manage_sensors_menu ;; esac } # Get themes source directory - prioritizes local script directory, then /opt/proxmorph # For PDM, looks for themes/pdm/ subdirectory first get_themes_source() { # 1. Check local script directory (prioritize local execution/development) local script_dir="" if [[ -n "${BASH_SOURCE[0]}" ]]; then script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" fi # Handle piped execution where BASH_SOURCE might be /dev/fd/* if [[ -n "$script_dir" && "$script_dir" != /dev/fd* && "$script_dir" != /proc/* && -d "${script_dir}/themes" ]]; then # PDM uses a separate theme subdirectory if [[ "$PRODUCT" == "PDM" && -d "${script_dir}/themes/pdm" ]]; then echo "${script_dir}/themes/pdm" return 0 fi echo "${script_dir}/themes" return 0 fi # 2. Check /opt/proxmorph (fallback to installed cache) if [[ -d "${INSTALL_DIR}/themes" ]]; then if [[ "$PRODUCT" == "PDM" && -d "${INSTALL_DIR}/themes/pdm" ]]; then echo "${INSTALL_DIR}/themes/pdm" return 0 fi echo "${INSTALL_DIR}/themes" return 0 fi return 1 } # Install all themes from themes directory install_themes() { print_info "Installing ProxMorph themes..." local themes_source=$(get_themes_source) if [[ -z "$themes_source" ]]; then print_info "Local themes not found, attempting to download latest release..." download_release themes_source=$(get_themes_source) if [[ -z "$themes_source" ]]; then print_error "Failed to locate themes even after download" exit 1 fi fi # Count themes local theme_count=$(find "$themes_source" -name "theme-*.css" 2>/dev/null | wc -l) if [[ $theme_count -eq 0 ]]; then print_error "No theme files found in $themes_source (looking for theme-*.css)" exit 1 fi print_info "Found $theme_count theme(s)" # Backup original files backup_files # PDM uses a completely different installation approach (CSS injection via index.hbs) if [[ "$PRODUCT" == "PDM" ]]; then # Sync to local cache if [[ "$themes_source" != "${INSTALL_DIR}/themes/pdm" ]]; then mkdir -p "${INSTALL_DIR}/themes/pdm" cp "$themes_source"/theme-*.css "${INSTALL_DIR}/themes/pdm/" 2>/dev/null || true fi install_pdm_themes "$themes_source" install_apt_hook echo "$VERSION" > "${INSTALL_DIR}/.version" echo "" print_status "ProxMorph PDM themes installed successfully!" echo "" print_info "To activate a theme:" print_info " 1. Open browser console (F12) on your PDM web UI" print_info " 2. Run: localStorage.setItem('proxmorph-theme', 'theme-dracula.css')" print_info " 3. Reload the page (Ctrl+Shift+R)" print_info "" print_info "Available themes:" for css_file in "$themes_source"/theme-*.css; do if [[ -f "$css_file" ]]; then local pname=$(basename "$css_file") local ptitle=$(get_theme_title "$css_file") print_theme " ${ptitle}: localStorage.setItem('proxmorph-theme', '${pname}')" fi done print_info "" print_info "To disable: localStorage.removeItem('proxmorph-theme') + reload" # Restart service print_info "Restarting ${PROXY_SERVICE} service in background..." nohup systemctl restart "${PROXY_SERVICE}" &>/dev/null & return 0 fi # PVE/PBS standard installation path # Create themes directory if not exists mkdir -p "$THEMES_DIR" mkdir -p "${INSTALL_DIR}/themes" # Process each theme for css_file in "$themes_source"/theme-*.css; do if [[ -f "$css_file" ]]; then theme_key=$(get_theme_key "$css_file") theme_title=$(get_theme_title "$css_file") # Copy CSS file to live Proxmox web directory cp "$css_file" "${THEMES_DIR}/" chmod 644 "${THEMES_DIR}/$(basename "$css_file")" # Sync to local cache so apt hook uses the newest files on update if [[ "$themes_source" != "${INSTALL_DIR}/themes" && "$themes_source" != "${INSTALL_DIR}/themes/pdm" ]]; then if [[ "$PRODUCT" == "PDM" ]]; then mkdir -p "${INSTALL_DIR}/themes/pdm" cp "$css_file" "${INSTALL_DIR}/themes/pdm/" else cp "$css_file" "${INSTALL_DIR}/themes/" fi fi # Register in theme_map (PDM may not use proxmoxlib.js theme_map) if [[ -f "$PROXMOXLIB_JS" ]]; then patch_theme_map "$theme_key" "$theme_title" elif [[ "$PRODUCT" == "PDM" ]]; then print_info "PDM detected — skipping proxmoxlib.js theme_map (not applicable)" fi fi done # Sync JavaScript patches to cache if installing locally if [[ -d "${themes_source}/patches" && "$themes_source" != "${INSTALL_DIR}/themes" ]]; then mkdir -p "${INSTALL_DIR}/themes/patches" cp "${themes_source}/patches"/*.js "${INSTALL_DIR}/themes/patches/" 2>/dev/null || true fi # Install apt hook for persistence across updates install_apt_hook # Install JavaScript patches (chart colors, etc.) install_js_patches # Write version file echo "$VERSION" > "${INSTALL_DIR}/.version" # Offer hardware sensor integration (PVE only) if [[ "$PRODUCT" == "PVE" ]]; then echo "" install_sensors fi echo "" print_status "ProxMorph themes installed successfully!" echo "" print_info "To apply a theme:" print_info " 1. Clear your browser cache (Ctrl+Shift+R)" print_info " 2. Click your username → Color Theme" print_info " 3. Select a ProxMorph theme from the dropdown" # Restart proxy service in background print_info "Restarting ${PROXY_SERVICE} service in background..." nohup systemctl restart "${PROXY_SERVICE}" &>/dev/null & } # Install a specific theme install_single_theme() { local theme_file="$1" if [[ ! -f "$theme_file" ]]; then print_error "Theme file not found: $theme_file" exit 1 fi backup_files mkdir -p "$THEMES_DIR" theme_key=$(get_theme_key "$theme_file") theme_title=$(get_theme_title "$theme_file") cp "$theme_file" "${THEMES_DIR}/" chmod 644 "${THEMES_DIR}/$(basename "$theme_file")" patch_theme_map "$theme_key" "$theme_title" print_status "Theme '${theme_title}' installed!" print_info "Restarting ${PROXY_SERVICE} service in background..." nohup systemctl restart "${PROXY_SERVICE}" &>/dev/null & } # Reinstall themes (after PVE update) reinstall_themes() { print_info "Reinstalling ProxMorph themes..." restore_packages install_themes } # Uninstall all themes uninstall_themes() { print_info "Uninstalling ProxMorph themes..." # PDM-specific uninstall if [[ "$PRODUCT" == "PDM" ]]; then remove_pdm_themes remove_apt_hook if [[ -d "$INSTALL_DIR" ]]; then rm -rf "$INSTALL_DIR" print_status "Removed install directory: $INSTALL_DIR" fi echo "" print_status "ProxMorph PDM themes uninstalled!" print_info "Clear your browser cache and localStorage to see the changes." print_info "Restarting ${PROXY_SERVICE}..." nohup systemctl restart "${PROXY_SERVICE}" &>/dev/null & return 0 fi # PVE/PBS uninstall # Find themes source local themes_source=$(get_themes_source) || THEMES_DIR # Remove CSS files for css_file in "$themes_source"/theme-*.css; do if [[ -f "$css_file" ]]; then target_file="${THEMES_DIR}/$(basename "$css_file")" if [[ -f "$target_file" ]]; then rm "$target_file" print_status "Removed: $(basename "$css_file")" fi fi done # Remove JavaScript patches remove_js_patches # Remove sensor patches remove_sensors # Remove apt hook remove_apt_hook # Restore original proxmoxlib.js restore_packages # Clean up install directory if [[ -d "$INSTALL_DIR" ]]; then rm -rf "$INSTALL_DIR" print_status "Removed install directory: $INSTALL_DIR" fi echo "" print_status "ProxMorph themes uninstalled!" print_info "Clear your browser cache to see the changes." } # List available themes list_themes() { print_info "Available ProxMorph Themes:" echo "" # Find themes source local themes_source=$(get_themes_source) if [[ -z "$themes_source" ]]; then print_error "Themes directory not found. Run 'update' first to download themes." return 1 fi for css_file in "$themes_source"/theme-*.css; do if [[ -f "$css_file" ]]; then theme_key=$(get_theme_key "$css_file") theme_title=$(get_theme_title "$css_file") # Check if installed if [[ -f "${THEMES_DIR}/$(basename "$css_file")" ]]; then echo -e " ${GREEN}●${NC} ${theme_title} (${theme_key}) - Installed" else echo -e " ${YELLOW}○${NC} ${theme_title} (${theme_key})" fi fi done echo "" } # Show status show_status() { print_info "ProxMorph Status:" echo "" # Show installed version if [[ -f "${INSTALL_DIR}/.version" ]]; then local current_ver=$(cat "${INSTALL_DIR}/.version") echo -e " Version: ${GREEN}v${current_ver}${NC}" else echo -e " Version: ${YELLOW}Unknown (local install)${NC}" fi # Dynamically check if our themes are registered in proxmoxlib.js local themes_source=$(get_themes_source) local is_patched=false if [[ -n "$themes_source" ]]; then for css_file in "${themes_source}"/theme-*.css; do if [[ -f "$css_file" ]]; then local theme_key=$(get_theme_key "$css_file") if grep -q "\"${theme_key}\":" "$PROXMOXLIB_JS" 2>/dev/null; then is_patched=true break fi fi done fi if [[ "$is_patched" == "true" ]]; then echo -e " Theme Map: ${GREEN}Patched${NC}" else echo -e " Theme Map: ${YELLOW}Not patched${NC}" fi # Count installed themes local installed=0 if [[ -n "$themes_source" ]]; then for css_file in "${themes_source}"/theme-*.css; do if [[ -f "${THEMES_DIR}/$(basename "$css_file")" ]]; then installed=$((installed + 1)) fi done fi echo -e " Installed: ${GREEN}${installed}${NC} theme(s)" # Backup status if [[ -f "${BACKUP_DIR}/proxmoxlib.js.original" ]]; then echo -e " Backup: ${GREEN}Available${NC}" else echo -e " Backup: ${YELLOW}Not created${NC}" fi # Apt hook status (persistence) if check_apt_hook; then echo -e " Auto-patch: ${GREEN}Enabled${NC} (persists across ${PRODUCT} updates)" else echo -e " Auto-patch: ${YELLOW}Not installed${NC}" fi # Sensor status (PVE only) if [[ "$PRODUCT" == "PVE" ]]; then manage_sensors status fi echo "" list_themes } # Main menu show_menu() { echo "" echo "Select an option:" echo " 1) Install themes" echo " 2) Update from Gitea (latest commit)" echo " 3) Reinstall themes (after update)" echo " 4) Uninstall themes" echo " 5) List themes" echo " 6) Show status" [[ "$PRODUCT" == "PVE" ]] && echo " 7) Manage sensors" echo " 0) Exit" echo "" read -p "Enter choice [0-7]: " choice case $choice in 1) install_themes ;; 2) download_release && install_themes ;; 3) reinstall_themes ;; 4) uninstall_themes ;; 5) list_themes ;; 6) show_status ;; 7) manage_sensors_menu ;; 0) exit 0 ;; *) print_error "Invalid option" ; show_menu ;; esac } # Parse command line arguments main() { check_root check_product case "${1:-}" in install) install_themes ;; update) download_release "${2:-}" install_themes ;; reinstall) reinstall_themes ;; uninstall) uninstall_themes ;; list) list_themes ;; status) show_status ;; check) check_updates ;; sensors) manage_sensors "${2:-status}" ;; *) show_menu ;; esac } main "$@"