From a7a5b54680423afe6deff6eeff336f8f3c38f8e5 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:33:12 +0100 Subject: [PATCH 01/19] example --- tools/addon/jellystat.sh | 374 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 tools/addon/jellystat.sh diff --git a/tools/addon/jellystat.sh b/tools/addon/jellystat.sh new file mode 100644 index 000000000..2afd4fa81 --- /dev/null +++ b/tools/addon/jellystat.sh @@ -0,0 +1,374 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://github.com/CyferShepard/Jellystat + +if ! command -v curl &>/dev/null; then + printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 + apt-get update >/dev/null 2>&1 + apt-get install -y curl >/dev/null 2>&1 +fi +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/core.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/tools.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/error_handler.func) + +# Enable error handling +set -Eeuo pipefail +trap 'error_handler' ERR + +# ============================================================================== +# CONFIGURATION +# ============================================================================== +APP="Jellystat" +APP_TYPE="addon" +INSTALL_PATH="/opt/jellystat" +CONFIG_PATH="/opt/jellystat/.env" +DEFAULT_PORT=3000 + +# Initialize all core functions (colors, formatting, icons, STD mode) +load_functions + +# ============================================================================== +# HEADER +# ============================================================================== +function header_info { + clear + cat <<"EOF" + __ ____ __ __ + / /__ / / /_ _______/ /_____ _/ /_ + __ / / _ \/ / / / / / ___/ __/ __ `/ __/ +/ /_/ / __/ / / /_/ (__ ) /_/ /_/ / /_ +\____/\___/_/_/\__, /____/\__/\__,_/\__/ + /____/ +EOF +} + +# ============================================================================== +# OS DETECTION +# ============================================================================== +if [[ -f "/etc/alpine-release" ]]; then + msg_error "Alpine is not supported for ${APP}. Use Debian/Ubuntu." + exit 1 +elif [[ -f "/etc/debian_version" ]]; then + OS="Debian" + SERVICE_PATH="/etc/systemd/system/jellystat.service" +else + echo -e "${CROSS} Unsupported OS detected. Exiting." + exit 1 +fi + +# ============================================================================== +# UNINSTALL +# ============================================================================== +function uninstall() { + msg_info "Uninstalling ${APP}" + systemctl disable --now jellystat.service &>/dev/null || true + rm -f "$SERVICE_PATH" + rm -rf "$INSTALL_PATH" + rm -f "/usr/local/bin/update_jellystat" + rm -f "$HOME/.jellystat" + msg_ok "${APP} has been uninstalled" + + # Ask about PostgreSQL database removal + echo "" + echo -n "${TAB}Also remove PostgreSQL database 'jellystat'? (y/N): " + read -r db_prompt + if [[ "${db_prompt,,}" =~ ^(y|yes)$ ]]; then + if command -v psql &>/dev/null; then + msg_info "Removing PostgreSQL database and user" + $STD sudo -u postgres psql -c "DROP DATABASE IF EXISTS jellystat;" &>/dev/null || true + $STD sudo -u postgres psql -c "DROP USER IF EXISTS jellystat;" &>/dev/null || true + msg_ok "Removed PostgreSQL database 'jellystat' and user 'jellystat'" + else + msg_warn "PostgreSQL not found - database may have been removed already" + fi + else + msg_warn "PostgreSQL database was NOT removed. Remove manually if needed:" + echo -e "${TAB} sudo -u postgres psql -c \"DROP DATABASE jellystat;\"" + echo -e "${TAB} sudo -u postgres psql -c \"DROP USER jellystat;\"" + fi +} + +# ============================================================================== +# UPDATE +# ============================================================================== +function update() { + if check_for_gh_release "jellystat" "CyferShepard/Jellystat"; then + msg_info "Stopping service" + systemctl stop jellystat.service &>/dev/null || true + msg_ok "Stopped service" + + msg_info "Backing up configuration" + cp "$CONFIG_PATH" /tmp/jellystat.env.bak 2>/dev/null || true + msg_ok "Backed up configuration" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "jellystat" "CyferShepard/Jellystat" "tarball" "latest" "$INSTALL_PATH" + + msg_info "Restoring configuration" + cp /tmp/jellystat.env.bak "$CONFIG_PATH" 2>/dev/null || true + rm -f /tmp/jellystat.env.bak + msg_ok "Restored configuration" + + msg_info "Installing dependencies" + cd "$INSTALL_PATH" + $STD npm install + msg_ok "Installed dependencies" + + msg_info "Building ${APP}" + $STD npm run build + msg_ok "Built ${APP}" + + msg_info "Starting service" + systemctl start jellystat + msg_ok "Started service" + msg_ok "Updated successfully" + exit + fi +} + +# ============================================================================== +# INSTALL +# ============================================================================== +function install() { + # Setup Node.js (only installs if not present or different version) + if command -v node &>/dev/null; then + msg_ok "Node.js already installed ($(node -v))" + else + NODE_VERSION="22" setup_nodejs + fi + + # Setup PostgreSQL (only installs if not present) + if command -v psql &>/dev/null; then + msg_ok "PostgreSQL already installed" + else + PG_VERSION="17" setup_postgresql + fi + + # Create database and user (skip if already exists) + local DB_NAME="jellystat" + local DB_USER="jellystat" + local DB_PASS + + msg_info "Setting up PostgreSQL database" + + # Check if database already exists + if sudo -u postgres psql -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw "$DB_NAME"; then + msg_warn "Database '${DB_NAME}' already exists - skipping creation" + echo -n "${TAB}Enter existing database password for '${DB_USER}': " + read -rs DB_PASS + echo "" + else + # Generate new password + DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c16) + + # Check if user exists, create if not + if sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" 2>/dev/null | grep -q 1; then + msg_info "User '${DB_USER}' exists, updating password" + $STD sudo -u postgres psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" || { + msg_error "Failed to update PostgreSQL user" + return 1 + } + else + $STD sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}';" || { + msg_error "Failed to create PostgreSQL user" + return 1 + } + fi + + # Create database (use template0 for UTF8 encoding compatibility) + $STD sudo -u postgres psql -c "CREATE DATABASE ${DB_NAME} WITH OWNER ${DB_USER} ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' TEMPLATE template0;" || { + msg_error "Failed to create PostgreSQL database" + return 1 + } + $STD sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};" || { + msg_error "Failed to grant privileges" + return 1 + } + + # Grant schema permissions (required for PostgreSQL 15+) + $STD sudo -u postgres psql -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO ${DB_USER};" || true + + # Configure pg_hba.conf for password authentication on localhost + local PG_HBA + PG_HBA=$(sudo -u postgres psql -tAc "SHOW hba_file;" 2>/dev/null | tr -d ' ') + if [[ -n "$PG_HBA" && -f "$PG_HBA" ]]; then + # Check if md5/scram-sha-256 auth is already configured for local connections + if ! grep -qE "^host\s+${DB_NAME}\s+${DB_USER}\s+127.0.0.1" "$PG_HBA"; then + msg_info "Configuring PostgreSQL authentication" + # Add password auth for jellystat user on localhost (before the default rules) + sed -i "/^# IPv4 local connections:/a host ${DB_NAME} ${DB_USER} 127.0.0.1/32 scram-sha-256" "$PG_HBA" + sed -i "/^# IPv4 local connections:/a host ${DB_NAME} ${DB_USER} ::1/128 scram-sha-256" "$PG_HBA" + # Reload PostgreSQL to apply changes + systemctl reload postgresql + msg_ok "Configured PostgreSQL authentication" + fi + fi + + msg_ok "Created PostgreSQL database '${DB_NAME}'" + fi + + # Generate JWT Secret + local JWT_SECRET + JWT_SECRET=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c32) + + # Force fresh download by removing version cache + rm -f "$HOME/.jellystat" + fetch_and_deploy_gh_release "jellystat" "CyferShepard/Jellystat" "tarball" "latest" "$INSTALL_PATH" + + msg_info "Installing dependencies" + cd "$INSTALL_PATH" + $STD npm install + msg_ok "Installed dependencies" + + msg_info "Building ${APP}" + $STD npm run build + msg_ok "Built ${APP}" + + msg_info "Creating configuration" + cat <"$CONFIG_PATH" +# Jellystat Configuration +# Database +POSTGRES_USER=${DB_USER} +POSTGRES_PASSWORD=${DB_PASS} +POSTGRES_IP=localhost +POSTGRES_PORT=5432 +POSTGRES_DB=${DB_NAME} + +# Security +JWT_SECRET=${JWT_SECRET} + +# Server +JS_LISTEN_IP=0.0.0.0 +JS_BASE_URL=/ +TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") + +# Optional: GeoLite for IP Geolocation +# JS_GEOLITE_ACCOUNT_ID= +# JS_GEOLITE_LICENSE_KEY= + +# Optional: Master Override (if you forget your password) +# JS_USER=admin +# JS_PASSWORD=admin + +# Optional: Minimum playback duration to record (seconds) +# MINIMUM_SECONDS_TO_INCLUDE_PLAYBACK=1 + +# Optional: Self-signed certificates +REJECT_SELF_SIGNED_CERTIFICATES=true +EOF + chmod 600 "$CONFIG_PATH" + msg_ok "Created configuration" + + msg_info "Creating service" + cat <"$SERVICE_PATH" +[Unit] +Description=Jellystat - Statistics for Jellyfin +After=network.target postgresql.service + +[Service] +Type=simple +User=root +WorkingDirectory=${INSTALL_PATH}/backend +EnvironmentFile=${CONFIG_PATH} +ExecStart=/usr/bin/node ${INSTALL_PATH}/backend/server.js +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + systemctl enable --now jellystat &>/dev/null + msg_ok "Created and started service" + + # Create update script (simple wrapper that calls this addon with type=update) + msg_info "Creating update script" + cat <<'UPDATEEOF' >/usr/local/bin/update_jellystat +#!/usr/bin/env bash +# Jellystat Update Script +type=update bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/jellystat.sh)" +UPDATEEOF + chmod +x /usr/local/bin/update_jellystat + msg_ok "Created update script (/usr/local/bin/update_jellystat)" + + # Save credentials + local CREDS_FILE="/root/jellystat.creds" + cat <"$CREDS_FILE" +Jellystat Credentials +===================== +Database User: ${DB_USER} +Database Password: ${DB_PASS} +Database Name: ${DB_NAME} +JWT Secret: ${JWT_SECRET} + +Web UI: http://${LOCAL_IP}:${DEFAULT_PORT} +EOF + chmod 600 "$CREDS_FILE" + + echo "" + msg_ok "${APP} is reachable at: ${BL}http://${LOCAL_IP}:${DEFAULT_PORT}${CL}" + msg_ok "Credentials saved to: ${BL}${CREDS_FILE}${CL}" + echo "" + msg_warn "On first access, you'll need to configure your Jellyfin server connection." +} + +# ============================================================================== +# MAIN +# ============================================================================== + +# Handle type=update (called from update script) +if [[ "${type:-}" == "update" ]]; then + header_info + if [[ -d "$INSTALL_PATH" && -f "$INSTALL_PATH/package.json" ]]; then + update + else + msg_error "${APP} is not installed. Nothing to update." + exit 1 + fi + exit 0 +fi + +header_info +get_lxc_ip + +# Check if already installed +if [[ -d "$INSTALL_PATH" && -f "$INSTALL_PATH/package.json" ]]; then + msg_warn "${APP} is already installed." + echo "" + + echo -n "${TAB}Uninstall ${APP}? (y/N): " + read -r uninstall_prompt + if [[ "${uninstall_prompt,,}" =~ ^(y|yes)$ ]]; then + uninstall + exit 0 + fi + + echo -n "${TAB}Update ${APP}? (y/N): " + read -r update_prompt + if [[ "${update_prompt,,}" =~ ^(y|yes)$ ]]; then + update + exit 0 + fi + + msg_warn "No action selected. Exiting." + exit 0 +fi + +# Fresh installation +msg_warn "${APP} is not installed." +echo "" +echo -e "${TAB}${INFO} This will install:" +echo -e "${TAB} - Node.js 22" +echo -e "${TAB} - PostgreSQL 17" +echo -e "${TAB} - Jellystat" +echo "" + +echo -n "${TAB}Install ${APP}? (y/N): " +read -r install_prompt +if [[ "${install_prompt,,}" =~ ^(y|yes)$ ]]; then + install +else + msg_warn "Installation cancelled. Exiting." + exit 0 +fi From b65090b315e426e23f19a3047a0f206750b7778d Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:41:43 +0100 Subject: [PATCH 02/19] Add CronMaster addon; remove legacy installers Replace legacy container/install scripts with a single tools/addon/cronmaster.sh addon. The new script consolidates install, update and uninstall flows, uses community-scripts core functions, fetches prebuilt GitHub releases, sets up Node.js (defaults to v22), creates a systemd service and an update helper (/usr/local/bin/update_cronmaster). Removed ct/cronmaster.sh and install/cronmaster-install.sh to avoid duplication and centralize maintenance. --- ct/cronmaster.sh | 45 ------ frontend/public/json/cronmaster.json | 35 +++++ install/cronmaster-install.sh | 75 --------- tools/addon/cronmaster.sh | 226 +++++++++++++++++++++++++++ 4 files changed, 261 insertions(+), 120 deletions(-) delete mode 100644 ct/cronmaster.sh create mode 100644 frontend/public/json/cronmaster.json delete mode 100644 install/cronmaster-install.sh create mode 100644 tools/addon/cronmaster.sh diff --git a/ct/cronmaster.sh b/ct/cronmaster.sh deleted file mode 100644 index 7a2e0ac50..000000000 --- a/ct/cronmaster.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env bash -source <(curl -fsSL https://git.community-scripts.org/community-scripts/ProxmoxVED/raw/branch/main/misc/build.func) -# Copyright (c) 2021-2026 community-scripts ORG -# Author: MickLesk (CanbiZ) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: - -APP="CRONMASTER" -var_tags="${var_tags:-}" -var_cpu="${var_cpu:-2}" -var_ram="${var_ram:-4096}" -var_disk="${var_disk:-8}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" -#var_fuse="${var_fuse:-no}" -#var_tun="${var_tun:-no}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - if [[ ! -d /opt/cronmaster ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - msg_info "Updating Debian LXC" - $STD apt update - $STD apt upgrade -y - msg_ok "Updated Debian LXC" - cleanup_lxc - exit -} - -start -build_container -description - -msg_ok "Completed successfully!" -msg_custom "🚀" "${GN}" "${APP} setup has been successfully initialized!" diff --git a/frontend/public/json/cronmaster.json b/frontend/public/json/cronmaster.json new file mode 100644 index 000000000..956ea3726 --- /dev/null +++ b/frontend/public/json/cronmaster.json @@ -0,0 +1,35 @@ +{ + "name": "CronMaster", + "slug": "cronmaster", + "categories": [ + 1 + ], + "date_created": "2026-02-17", + "type": "pve", + "updateable": true, + "privileged": false, + "interface_port": 3000, + "documentation": "https://github.com/fccview/cronmaster", + "website": "https://github.com/fccview/cronmaster", + "logo": "https://raw.githubusercontent.com/fccview/cronmaster/main/public/logo.png", + "config_path": "/opt/cronmaster/.env", + "description": "Self-hosted cron job scheduler with web UI, live logs, auth and prebuilt binaries provided upstream.", + "install_methods": [ + { + "type": "default", + "script": "tools/addon/cronmaster.sh", + "resources": { + "cpu": null, + "ram": null, + "hdd": null, + "os": null, + "version": null + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [] +} \ No newline at end of file diff --git a/install/cronmaster-install.sh b/install/cronmaster-install.sh deleted file mode 100644 index 16b10816a..000000000 --- a/install/cronmaster-install.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2026 community-scripts ORG -# Author: Slaviša Arežina (tremor021) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/fccview/cronmaster - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os -setup_hwaccel - -msg_info "Installing dependencies" -$STD apt install -y pciutils -msg_ok "Installed dependencies" - -NODE_VERSION="24" NODE_MODULE="yarn" setup_nodejs - -setup_deb822_repo \ - "docker" \ - "https://download.docker.com/linux/debian/gpg" \ - "https://download.docker.com/linux/debian" \ - "trixie" \ - "stable" -$STD apt install -y docker-ce-cli -fetch_and_deploy_gh_release "cronmaster" "fccview/cronmaster" "tarball" - -msg_info "Setting up CronMaster" -AUTH_PASS="$(openssl rand -base64 18 | cut -c1-13)" -cd /opt/cronmaster -$STD yarn --frozen-lockfile -export NEXT_TELEMETRY_DISABLED=1 -$STD yarn build -cat </opt/cronmaster/.env -NODE_ENV=production -APP_URL= -LOCALE= -HOME= -AUTH_PASSWORD=${AUTH_PASS} -PORT=3000 -HOSTNAME="0.0.0.0" -NEXT_TELEMETRY_DISABLED=1 -EOF -{ - echo "CronMaster Credentials:" - echo "" - echo "Password: $AUTH_PASS" -}>>~/cronmaster.creds -msg_ok "Setup CronMaster" - -msg_info "Creating Service" -cat </etc/systemd/system/cronmaster.service -[Unit] -Description=CronMaster Service -After=network.target - -[Service] -EnvironmentFile=/opt/cronmaster/.env -WorkingDirectory=/opt/cronmaster -ExecStart=/usr/bin/yarn start -Restart=always - -[Install] -WantedBy=multi-user.target -EOF -systemctl start --now -q cronmaster -msg_info "Created Service" - -motd_ssh -customize -cleanup_lxc diff --git a/tools/addon/cronmaster.sh b/tools/addon/cronmaster.sh new file mode 100644 index 000000000..faf2780be --- /dev/null +++ b/tools/addon/cronmaster.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env bash + +# Copyright (c) 2021-2026 community-scripts ORG +# Author: MickLesk (CanbiZ) +# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE +# Source: https://github.com/fccview/cronmaster + +if ! command -v curl &>/dev/null; then + printf "\r\e[2K%b" '\033[93m Setup Source \033[m' >&2 + apt-get update >/dev/null 2>&1 + apt-get install -y curl >/dev/null 2>&1 +fi +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/core.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/tools.func) +source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/error_handler.func) + +# Enable error handling +set -Eeuo pipefail +trap 'error_handler' ERR + +# ============================================================================== +# CONFIGURATION +# ============================================================================== +APP="CronMaster" +APP_TYPE="addon" +INSTALL_PATH="/opt/cronmaster" +CONFIG_PATH="/opt/cronmaster/.env" +DEFAULT_PORT=3000 + +# Initialize all core functions (colors, formatting, icons, STD mode) +load_functions + +# ============================================================================== +# HEADER +# ============================================================================== +function header_info { + clear + cat <<"EOF" + ______ __ ___ __ + / ____/________ ____ / |/ /___ ______/ /____ _____ + / / / ___/ __ \/ __ \/ /|_/ / __ `/ ___/ __/ _ \/ ___/ +/ /___/ / / /_/ / / / / / / / /_/ (__ ) /_/ __/ / +\____/_/ \____/_/ /_/_/ /_/\__,_/____/\__/\___/_/ + +EOF +} + +# ============================================================================== +# OS DETECTION +# ============================================================================== +if [[ -f "/etc/alpine-release" ]]; then + msg_error "Alpine is not supported for ${APP}. Use Debian/Ubuntu." + exit 1 +elif [[ -f "/etc/debian_version" ]]; then + OS="Debian" + SERVICE_PATH="/etc/systemd/system/cronmaster.service" +else + echo -e "${CROSS} Unsupported OS detected. Exiting." + exit 1 +fi + +# ============================================================================== +# UNINSTALL +# ============================================================================== +function uninstall() { + msg_info "Uninstalling ${APP}" + systemctl disable --now cronmaster.service &>/dev/null || true + rm -f "$SERVICE_PATH" + rm -rf "$INSTALL_PATH" + rm -f "/usr/local/bin/update_cronmaster" + rm -f "$HOME/.cronmaster" + msg_ok "${APP} has been uninstalled" +} + +# ============================================================================== +# UPDATE +# ============================================================================== +function update() { + if check_for_gh_release "cronmaster" "fccview/cronmaster"; then + msg_info "Stopping service" + systemctl stop cronmaster.service &>/dev/null || true + msg_ok "Stopped service" + + msg_info "Backing up configuration" + cp "$CONFIG_PATH" /tmp/cronmaster.env.bak 2>/dev/null || true + msg_ok "Backed up configuration" + + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "cronmaster" "fccview/cronmaster" "prebuild" "latest" "$INSTALL_PATH" "cronmaster_*_prebuild.tar.gz" + + msg_info "Restoring configuration" + cp /tmp/cronmaster.env.bak "$CONFIG_PATH" 2>/dev/null || true + rm -f /tmp/cronmaster.env.bak + msg_ok "Restored configuration" + + msg_info "Starting service" + systemctl start cronmaster + msg_ok "Started service" + msg_ok "Updated successfully" + exit + fi +} + +# ============================================================================== +# INSTALL +# ============================================================================== +function install() { + # Setup Node.js (only installs if not present or different version) + if command -v node &>/dev/null; then + msg_ok "Node.js already installed ($(node -v))" + else + NODE_VERSION="22" setup_nodejs + fi + + # Force fresh download by removing version cache + rm -f "$HOME/.cronmaster" + fetch_and_deploy_gh_release "cronmaster" "fccview/cronmaster" "prebuild" "latest" "$INSTALL_PATH" "cronmaster_*_prebuild.tar.gz" + + local AUTH_PASS + AUTH_PASS="$(openssl rand -base64 18 | cut -c1-13)" + + msg_info "Creating configuration" + cat <"$CONFIG_PATH" +NODE_ENV=production +AUTH_PASSWORD=${AUTH_PASS} +PORT=${DEFAULT_PORT} +HOSTNAME=0.0.0.0 +NEXT_TELEMETRY_DISABLED=1 +EOF + chmod 600 "$CONFIG_PATH" + msg_ok "Created configuration" + + msg_info "Creating service" + cat <"$SERVICE_PATH" +[Unit] +Description=CronMaster Service +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=${INSTALL_PATH} +EnvironmentFile=${CONFIG_PATH} +ExecStart=/usr/bin/node ${INSTALL_PATH}/server.js +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-target.target +EOF + systemctl enable --now cronmaster &>/dev/null + msg_ok "Created and started service" + + # Create update script + msg_info "Creating update script" + cat <<'UPDATEEOF' >/usr/local/bin/update_cronmaster +#!/usr/bin/env bash +# CronMaster Update Script +type=update bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/tools/addon/cronmaster.sh)" +UPDATEEOF + chmod +x /usr/local/bin/update_cronmaster + msg_ok "Created update script (/usr/local/bin/update_cronmaster)" + + echo "" + msg_ok "${APP} is reachable at: ${BL}http://${LOCAL_IP}:${DEFAULT_PORT}${CL}" + msg_ok "Password: ${BL}${AUTH_PASS}${CL}" + echo "" +} + +# ============================================================================== +# MAIN +# ============================================================================== + +# Handle type=update (called from update script) +if [[ "${type:-}" == "update" ]]; then + header_info + if [[ -d "$INSTALL_PATH" ]]; then + update + else + msg_error "${APP} is not installed. Nothing to update." + exit 1 + fi + exit 0 +fi + +header_info +get_lxc_ip + +# Check if already installed +if [[ -d "$INSTALL_PATH" && -n "$(ls -A "$INSTALL_PATH" 2>/dev/null)" ]]; then + msg_warn "${APP} is already installed." + echo "" + + echo -n "${TAB}Uninstall ${APP}? (y/N): " + read -r uninstall_prompt + if [[ "${uninstall_prompt,,}" =~ ^(y|yes)$ ]]; then + uninstall + exit 0 + fi + + echo -n "${TAB}Update ${APP}? (y/N): " + read -r update_prompt + if [[ "${update_prompt,,}" =~ ^(y|yes)$ ]]; then + update + exit 0 + fi + + msg_warn "No action selected. Exiting." + exit 0 +fi + +# Fresh installation +msg_warn "${APP} is not installed." +echo "" +echo -e "${TAB}${INFO} This will install:" +echo -e "${TAB} - Node.js 22" +echo -e "${TAB} - CronMaster (prebuild)" +echo "" + +echo -n "${TAB}Install ${APP}? (y/N): " +read -r install_prompt +if [[ "${install_prompt,,}" =~ ^(y|yes)$ ]]; then + install +else + msg_warn "Installation cancelled. Exiting." + exit 0 +fi From 3eaa0ecf10f651ad7e46a150e92194572f02566d Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:48:17 +0100 Subject: [PATCH 03/19] switch cronmmaster icon --- frontend/public/json/cronmaster.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/public/json/cronmaster.json b/frontend/public/json/cronmaster.json index 956ea3726..690b7227f 100644 --- a/frontend/public/json/cronmaster.json +++ b/frontend/public/json/cronmaster.json @@ -11,7 +11,7 @@ "interface_port": 3000, "documentation": "https://github.com/fccview/cronmaster", "website": "https://github.com/fccview/cronmaster", - "logo": "https://raw.githubusercontent.com/fccview/cronmaster/main/public/logo.png", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/cr-nmaster.webp", "config_path": "/opt/cronmaster/.env", "description": "Self-hosted cron job scheduler with web UI, live logs, auth and prebuilt binaries provided upstream.", "install_methods": [ From 6c43b624c14ff493b42b116aaed27ec743e8a480 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:23:26 +0100 Subject: [PATCH 04/19] Enhance Proxmox dependency-check scripts Update frontend docs and significantly refactor dependency-check tooling. - frontend: set supported version to "PVE 8.x / 9.x" and add info about --install/--status/--uninstall flags. - Add new tools/pve/dependency-check copy.sh (installer wrapper). - Rework tools/pve/dependency-check.sh: add CLI (install/status/uninstall), PVE version detection/validation, improved logging/colors, safer config parsing, more robust storage checks, validated tag handling (dep_ping/dep_tcp), portable TCP/ping checks, and wait/timeout helper. - Improve applicator script handling (ignore list, avoid overwriting other hookscripts), update systemd units (PathExistsGlob, unit binding), and implement uninstall to remove assignments and installed files. These changes harden lifecycle management and make installation/cleanup and runtime checks more robust and observable. --- frontend/public/json/dependency-check.json | 8 +- tools/pve/dependency-check copy.sh | 361 +++++++++++++ tools/pve/dependency-check.sh | 564 +++++++++++++-------- 3 files changed, 722 insertions(+), 211 deletions(-) create mode 100644 tools/pve/dependency-check copy.sh diff --git a/frontend/public/json/dependency-check.json b/frontend/public/json/dependency-check.json index 5f9307da1..22f08ae79 100644 --- a/frontend/public/json/dependency-check.json +++ b/frontend/public/json/dependency-check.json @@ -23,7 +23,7 @@ "ram": null, "hdd": null, "os": null, - "version": null + "version": "PVE 8.x / 9.x" } } ], @@ -36,6 +36,10 @@ "text": "Execute within the Proxmox shell", "type": "info" }, + { + "text": "The script supports --install (default), --status and --uninstall for clean lifecycle management.", + "type": "info" + }, { "text": "To wait until a certain host is available, tag the VM or container with `dep_ping_` where `` is the name or IP of the host to ping. The script will wait until the host is reachable before proceeding with the startup.", "type": "info" @@ -45,4 +49,4 @@ "type": "info" } ] -} +} \ No newline at end of file diff --git a/tools/pve/dependency-check copy.sh b/tools/pve/dependency-check copy.sh new file mode 100644 index 000000000..d0a328912 --- /dev/null +++ b/tools/pve/dependency-check copy.sh @@ -0,0 +1,361 @@ +#!/usr/bin/env bash + +# Copyright (c) 2023 community-scripts ORG +# This script is designed to install the Proxmox Dependency Check Hookscript. +# It sets up a dependency-checking hookscript and automates its +# application to all new and existing guests using a systemd watcher. +# License: MIT + +function header_info { + clear + cat <<"EOF" + ____ _ ____ _ _ + | _ \ ___ _ __ ___ _ __ __| | ___ _ __ ___ _ _ / ___| |__ ___ ___| | __ + | | | |/ _ \ '_ \ / _ \ '_ \ / _` |/ _ \ '_ \ / __| | | | | | '_ \ / _ \/ __| |/ / + | |_| | __/ |_) | __/ | | | (_| | __/ | | | (__| |_| | |___| | | | __/ (__| < + |____/ \___| .__/ \___|_| |_|\__,_|\___|_| |_|\___|\__, |\____|_| |_|\___|\___|_|\_\ + |_| |___/ +EOF +} + +# Color variables +YW=$(echo "\033[33m") +GN=$(echo "\033[1;92m") +RD=$(echo "\033[01;31m") +CL=$(echo "\033[m") +BFR="\\r\\033[K" +HOLD=" " +CM="${GN}✓${CL}" +CROSS="${RD}✗${CL}" + +# Spinner for progress indication (simplified) +spinner() { + local pid=$! + local delay=0.1 + local spinstr='|/-\' + while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do + local temp=${spinstr#?} + printf " [%c] " "$spinstr" + local spinstr=$temp${spinstr%"$temp"} + sleep $delay + printf "\b\b\b\b\b\b" + done + printf " \b\b\b\b" +} + +# Message functions +msg_info() { + echo -ne " ${YW}›${CL} $1..." +} + +msg_ok() { + echo -e "${BFR} ${CM} $1${CL}" +} + +msg_error() { + echo -e "${BFR} ${CROSS} $1${CL}" +} +# --- End of base script functions --- + +# --- Installation Functions --- + +# Function to create the actual hookscript that runs before guest startup +create_dependency_hookscript() { + msg_info "Creating dependency-check hookscript" + mkdir -p /var/lib/vz/snippets + cat <<'EOF' >/var/lib/vz/snippets/dependency-check.sh +#!/bin/bash +# Proxmox Hookscript for Pre-Start Dependency Checking +# Works for both QEMU VMs and LXC Containers + +# --- Configuration --- +POLL_INTERVAL=5 # Seconds to wait between checks +MAX_ATTEMPTS=60 # Max number of attempts before failing (60 * 5s = 5 minutes) +# --- End Configuration --- + +VMID=$1 +PHASE=$2 + +# Function for logging to syslog with a consistent format +log() { + echo "[hookscript-dep-check] VMID $VMID: $1" +} + +# This script only runs in the 'pre-start' phase +if [ "$PHASE" != "pre-start" ]; then + exit 0 +fi + +log "--- Starting Pre-Start Dependency Check ---" + +# --- Determine Guest Type (QEMU or LXC) --- +GUEST_TYPE="" +CONFIG_CMD="" +if qm config "$VMID" >/dev/null 2>&1; then + GUEST_TYPE="qemu" + CONFIG_CMD="qm config" + log "Guest type is QEMU (VM)." +elif pct config "$VMID" >/dev/null 2>&1; then + GUEST_TYPE="lxc" + CONFIG_CMD="pct config" + log "Guest type is LXC (Container)." +else + log "ERROR: Could not determine guest type for $VMID. Aborting." + exit 1 +fi + +GUEST_CONFIG=$($CONFIG_CMD "$VMID") + +# --- 1. Storage Availability Check --- +log "Checking storage availability..." +# Grep for all disk definitions (scsi, sata, virtio, ide, rootfs, mp) +# and extract the storage identifier (the field between the colons). +# Sort -u gets the unique list of storage pools. +STORAGE_IDS=$(echo "$GUEST_CONFIG" | grep -E '^(scsi|sata|virtio|ide|rootfs|mp)[0-9]*:' | awk -F'[:]' '{print $2}' | awk '{print$1}' | sort -u) + +if [ -z "$STORAGE_IDS" ]; then + log "No storage dependencies found to check." +else + for STORAGE_ID in $STORAGE_IDS; do + log "Checking status of storage: '$STORAGE_ID'" + ATTEMPTS=0 + while true; do + # Grep for the storage ID line in pvesm status and check the 'Active' column (3rd column) + STATUS=$(pvesm status | grep "^\s*$STORAGE_ID\s" | awk '{print $3}') + if [ "$STATUS" == "active" ]; then + log "Storage '$STORAGE_ID' is active." + break + fi + + ATTEMPTS=$((ATTEMPTS + 1)) + if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then + log "ERROR: Timeout waiting for storage '$STORAGE_ID' to become active. Aborting start." + exit 1 + fi + + log "Storage '$STORAGE_ID' is not active (current status: '${STATUS:-inactive/unknown}'). Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})" + sleep $POLL_INTERVAL + done + done +fi +log "All storage dependencies are met." + + +# --- 2. Custom Tag-Based Dependency Check --- +log "Checking for custom tag-based dependencies..." +TAGS=$(echo "$GUEST_CONFIG" | grep '^tags:' | awk '{print $2}') + +if [ -z "$TAGS" ]; then + log "No tags found. Skipping custom dependency check." +else + # Replace colons with spaces to loop through tags + for TAG in ${TAGS//;/ }; do + # Check if the tag matches our dependency format 'dep_*' + if [[ $TAG == dep_* ]]; then + log "Found dependency tag: '$TAG'" + + # Split tag into parts using underscore as delimiter + IFS='_' read -ra PARTS <<< "$TAG" + DEP_TYPE="${PARTS[1]}" + + ATTEMPTS=0 + while true; do + CHECK_PASSED=false + case "$DEP_TYPE" in + "tcp") + HOST="${PARTS[2]}" + PORT="${PARTS[3]}" + if [ -z "$HOST" ] || [ -z "$PORT" ]; then + log "ERROR: Malformed TCP dependency tag '$TAG'. Skipping." + CHECK_PASSED=true # Skip to avoid infinite loop + # nc -z is great for this. -w sets a timeout. + elif nc -z -w 2 "$HOST" "$PORT"; then + log "TCP dependency met: Host $HOST port $PORT is open." + CHECK_PASSED=true + fi + ;; + + "ping") + HOST="${PARTS[2]}" + if [ -z "$HOST" ]; then + log "ERROR: Malformed PING dependency tag '$TAG'. Skipping." + CHECK_PASSED=true # Skip to avoid infinite loop + # ping -c 1 (one packet) -W 2 (2-second timeout) + elif ping -c 1 -W 2 "$HOST" >/dev/null 2>&1; then + log "Ping dependency met: Host $HOST is reachable." + CHECK_PASSED=true + fi + ;; + + *) + log "WARNING: Unknown dependency type '$DEP_TYPE' in tag '$TAG'. Ignoring." + CHECK_PASSED=true # Mark as passed to avoid getting stuck + ;; + esac + + if $CHECK_PASSED; then + break + fi + + ATTEMPTS=$((ATTEMPTS + 1)) + if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then + log "ERROR: Timeout waiting for dependency '$TAG'. Aborting start." + exit 1 + fi + + log "Dependency '$TAG' not met. Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})" + sleep $POLL_INTERVAL + done + fi + done +fi + +log "All custom dependencies are met." +log "--- Dependency Check Complete. Proceeding with start. ---" +exit 0 +EOF + chmod +x /var/lib/vz/snippets/dependency-check.sh + msg_ok "Created dependency-check hookscript" +} + +# Function to create the config file for exclusions +create_exclusion_config() { + msg_info "Creating exclusion configuration file" + if [ -f /etc/default/pve-auto-hook ]; then + msg_ok "Exclusion file already exists, skipping." + else + cat <<'EOF' >/etc/default/pve-auto-hook +# +# Configuration for the Proxmox Automatic Hookscript Applicator +# +# Add VM or LXC IDs here to prevent the hookscript from being added. +# Separate IDs with spaces. +# +# Example: +# IGNORE_IDS="9000 9001 105" +# + +IGNORE_IDS="" +EOF + msg_ok "Created exclusion configuration file" + fi +} + +# Function to create the script that applies the hook +create_applicator_script() { + msg_info "Creating the hookscript applicator script" + cat <<'EOF' >/usr/local/bin/pve-apply-hookscript.sh +#!/bin/bash +HOOKSCRIPT_VOLUME_ID="local:snippets/dependency-check.sh" +CONFIG_FILE="/etc/default/pve-auto-hook" +LOG_TAG="pve-auto-hook-list" + +log() { + systemd-cat -t "$LOG_TAG" <<< "$1" +} + +if [ -f "$CONFIG_FILE" ]; then + source "$CONFIG_FILE" +fi + +# Process QEMU VMs +qm list | awk 'NR>1 {print $1}' | while read -r VMID; do + is_ignored=false + for id_to_ignore in $IGNORE_IDS; do + if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi + done + if $is_ignored; then continue; fi + if qm config "$VMID" | grep -q '^hookscript:'; then continue; fi + log "Hookscript not found for VM $VMID. Applying..." + qm set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID" +done + +# Process LXC Containers +pct list | awk 'NR>1 {print $1}' | while read -r VMID; do + is_ignored=false + for id_to_ignore in $IGNORE_IDS; do + if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi + done + if $is_ignored; then continue; fi + if pct config "$VMID" | grep -q '^hookscript:'; then continue; fi + log "Hookscript not found for LXC $VMID. Applying..." + pct set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID" +done +EOF + chmod +x /usr/local/bin/pve-apply-hookscript.sh + msg_ok "Created applicator script" +} + +# Function to set up the systemd watcher and service +create_systemd_units() { + msg_info "Creating systemd watcher and service units" + cat <<'EOF' >/etc/systemd/system/pve-auto-hook.path +[Unit] +Description=Watch for new Proxmox guest configs to apply hookscript + +[Path] +PathModified=/etc/pve/qemu-server/ +PathModified=/etc/pve/lxc/ + +[Install] +WantedBy=multi-user.target +EOF + + cat <<'EOF' >/etc/systemd/system/pve-auto-hook.service +[Unit] +Description=Automatically add hookscript to new Proxmox guests + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/pve-apply-hookscript.sh +EOF + msg_ok "Created systemd units" +} + +# --- Main Execution --- +header_info + +if ! command -v pveversion >/dev/null 2>&1; then + msg_error "This script must be run on a Proxmox VE host." + exit 1 +fi + +echo -e "\nThis script will install a service to automatically apply a" +echo -e "dependency-checking hookscript to all new and existing Proxmox guests." +echo -e "${YW}This includes creating files in:${CL}" +echo -e " - /var/lib/vz/snippets/" +echo -e " - /usr/local/bin/" +echo -e " - /etc/default/" +echo -e " - /etc/systemd/system/\n" + +read -p "Do you want to proceed with the installation? (y/n): " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]]; then + msg_error "Installation cancelled." + exit 1 +fi + +echo -e "\n" +create_dependency_hookscript +create_exclusion_config +create_applicator_script +create_systemd_units + +msg_info "Reloading systemd and enabling the watcher" +(systemctl daemon-reload && systemctl enable --now pve-auto-hook.path) >/dev/null 2>&1 & +spinner +msg_ok "Systemd watcher enabled and running" + +msg_info "Performing initial run to update existing guests" +/usr/local/bin/pve-apply-hookscript.sh >/dev/null 2>&1 & +spinner +msg_ok "Initial run complete" + +echo -e "\n\n${GN}Installation successful!${CL}" +echo -e "The service is now active and will monitor for new guests." +echo -e "To ${YW}exclude${CL} a VM or LXC, add its ID to the ${YW}IGNORE_IDS${CL} variable in:" +echo -e " ${YW}/etc/default/pve-auto-hook${CL}" +echo -e "\nYou can monitor the service's activity with:" +echo -e " ${YW}journalctl -fu pve-auto-hook.service${CL}\n" + +exit 0 diff --git a/tools/pve/dependency-check.sh b/tools/pve/dependency-check.sh index b7798d95a..36418dbc1 100644 --- a/tools/pve/dependency-check.sh +++ b/tools/pve/dependency-check.sh @@ -1,10 +1,9 @@ #!/usr/bin/env bash -# Copyright (c) 2023 community-scripts ORG -# This script is designed to install the Proxmox Dependency Check Hookscript. -# It sets up a dependency-checking hookscript and automates its -# application to all new and existing guests using a systemd watcher. -# License: MIT +# Copyright (c) 2023-2026 community-scripts ORG +# Author: MickLesk | Maintainer: community-scripts +# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE +# Source: https://www.proxmox.com/ function header_info { clear @@ -18,195 +17,220 @@ function header_info { EOF } -# Color variables YW=$(echo "\033[33m") GN=$(echo "\033[1;92m") RD=$(echo "\033[01;31m") +BL=$(echo "\033[36m") CL=$(echo "\033[m") BFR="\\r\\033[K" -HOLD=" " -CM="${GN}✓${CL}" -CROSS="${RD}✗${CL}" +CM="${GN}✔️${CL}" +CROSS="${RD}✖️${CL}" +INFO="${BL}ℹ️${CL}" -# Spinner for progress indication (simplified) -spinner() { - local pid=$! - local delay=0.1 - local spinstr='|/-\' - while [ "$(ps a | awk '{print $1}' | grep $pid)" ]; do - local temp=${spinstr#?} - printf " [%c] " "$spinstr" - local spinstr=$temp${spinstr%"$temp"} - sleep $delay - printf "\b\b\b\b\b\b" - done - printf " \b\b\b\b" -} - -# Message functions msg_info() { - echo -ne " ${YW}›${CL} $1..." + local msg="$1" + echo -e "${INFO} ${YW}${msg}...${CL}" } msg_ok() { - echo -e "${BFR} ${CM} $1${CL}" + local msg="$1" + echo -e "${BFR} ${CM} ${GN}${msg}${CL}" } msg_error() { - echo -e "${BFR} ${CROSS} $1${CL}" + local msg="$1" + echo -e "${BFR} ${CROSS} ${RD}${msg}${CL}" } -# --- End of base script functions --- +SCRIPT_NAME="$(basename "$0")" +HOOKSCRIPT_FILE="/var/lib/vz/snippets/dependency-check.sh" +HOOKSCRIPT_VOLUME_ID="local:snippets/dependency-check.sh" +CONFIG_FILE="/etc/default/pve-auto-hook" +APPLICATOR_FILE="/usr/local/bin/pve-apply-hookscript.sh" +PATH_UNIT_FILE="/etc/systemd/system/pve-auto-hook.path" +SERVICE_UNIT_FILE="/etc/systemd/system/pve-auto-hook.service" -# --- Installation Functions --- +function print_usage { + cat </dev/null 2>&1; then + msg_error "This script must be run on a Proxmox VE host" + exit 1 + fi + + local pve_version major + pve_version=$(pveversion | grep -oE 'pve-manager/[0-9.]+' | cut -d'/' -f2) + major=$(echo "$pve_version" | cut -d'.' -f1) + + if [[ -z "$major" ]] || ! [[ "$major" =~ ^[0-9]+$ ]]; then + msg_error "Unable to detect a supported Proxmox version" + exit 1 + fi + + if [[ "$major" -lt 8 ]] || [[ "$major" -gt 9 ]]; then + msg_error "Supported on Proxmox VE 8.x and 9.x (detected: $pve_version)" + exit 1 + fi + + msg_ok "Proxmox VE $pve_version detected" +} + +function confirm_action { + local prompt="$1" + read -r -p "$prompt (y/n): " -n 1 REPLY + echo + [[ "$REPLY" =~ ^[Yy]$ ]] +} -# Function to create the actual hookscript that runs before guest startup create_dependency_hookscript() { - msg_info "Creating dependency-check hookscript" - mkdir -p /var/lib/vz/snippets - cat <<'EOF' > /var/lib/vz/snippets/dependency-check.sh + msg_info "Creating dependency-check hookscript" + mkdir -p /var/lib/vz/snippets + cat <<'EOF' >/var/lib/vz/snippets/dependency-check.sh #!/bin/bash # Proxmox Hookscript for Pre-Start Dependency Checking # Works for both QEMU VMs and LXC Containers -# --- Configuration --- POLL_INTERVAL=5 # Seconds to wait between checks MAX_ATTEMPTS=60 # Max number of attempts before failing (60 * 5s = 5 minutes) -# --- End Configuration --- VMID=$1 PHASE=$2 -# Function for logging to syslog with a consistent format log() { - echo "[hookscript-dep-check] VMID $VMID: $1" + logger -t hookscript-dep-check "VMID $VMID: $1" +} + +has_cmd() { + command -v "$1" >/dev/null 2>&1 +} + +check_tcp() { + local host="$1" + local port="$2" + + if has_cmd nc; then + nc -z -w 2 "$host" "$port" >/dev/null 2>&1 + return $? + fi + + timeout 2 bash -c "/dev/null 2>&1 +} + +wait_until() { + local description="$1" + local check_cmd="$2" + + local attempts=0 + while true; do + if eval "$check_cmd"; then + log "$description" + return 0 + fi + + attempts=$((attempts + 1)) + if [ "$attempts" -ge "$MAX_ATTEMPTS" ]; then + log "ERROR: Timeout waiting for condition: $description" + return 1 + fi + + log "Waiting ${POLL_INTERVAL}s for condition: $description (Attempt ${attempts}/${MAX_ATTEMPTS})" + sleep "$POLL_INTERVAL" + done } -# This script only runs in the 'pre-start' phase if [ "$PHASE" != "pre-start" ]; then exit 0 fi log "--- Starting Pre-Start Dependency Check ---" -# --- Determine Guest Type (QEMU or LXC) --- -GUEST_TYPE="" -CONFIG_CMD="" if qm config "$VMID" >/dev/null 2>&1; then - GUEST_TYPE="qemu" - CONFIG_CMD="qm config" + CONFIG_CMD=(qm config "$VMID") log "Guest type is QEMU (VM)." elif pct config "$VMID" >/dev/null 2>&1; then - GUEST_TYPE="lxc" - CONFIG_CMD="pct config" + CONFIG_CMD=(pct config "$VMID") log "Guest type is LXC (Container)." else log "ERROR: Could not determine guest type for $VMID. Aborting." exit 1 fi -GUEST_CONFIG=$($CONFIG_CMD "$VMID") +GUEST_CONFIG=$("${CONFIG_CMD[@]}") -# --- 1. Storage Availability Check --- log "Checking storage availability..." -# Grep for all disk definitions (scsi, sata, virtio, ide, rootfs, mp) -# and extract the storage identifier (the field between the colons). -# Sort -u gets the unique list of storage pools. -STORAGE_IDS=$(echo "$GUEST_CONFIG" | grep -E '^(scsi|sata|virtio|ide|rootfs|mp)[0-9]*:' | awk -F'[:]' '{print $2}' | awk '{print$1}' | sort -u) +STORAGE_IDS=$(echo "$GUEST_CONFIG" | awk -F':' ' + /^(scsi|sata|virtio|ide|efidisk|tpmstate|unused|rootfs|mp)[0-9]*:/ { + val=$2 + gsub(/^[[:space:]]+/, "", val) + split(val, parts, ",") + storage=parts[1] + + # Skip bind-mount style paths and empty values + if (storage == "" || storage ~ /^\//) next + + print storage + } +' | sort -u) if [ -z "$STORAGE_IDS" ]; then log "No storage dependencies found to check." else for STORAGE_ID in $STORAGE_IDS; do - log "Checking status of storage: '$STORAGE_ID'" - ATTEMPTS=0 - while true; do - # Grep for the storage ID line in pvesm status and check the 'Active' column (3rd column) - STATUS=$(pvesm status | grep "^\s*$STORAGE_ID\s" | awk '{print $3}') - if [ "$STATUS" == "active" ]; then - log "Storage '$STORAGE_ID' is active." - break - fi + STATUS=$(pvesm status 2>/dev/null | awk -v id="$STORAGE_ID" '$1 == id { print $3; exit }') - ATTEMPTS=$((ATTEMPTS + 1)) - if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then - log "ERROR: Timeout waiting for storage '$STORAGE_ID' to become active. Aborting start." - exit 1 - fi + if [ -z "$STATUS" ]; then + log "WARNING: Storage '$STORAGE_ID' not found in 'pvesm status'. Skipping this dependency." + continue + fi - log "Storage '$STORAGE_ID' is not active (current status: '${STATUS:-inactive/unknown}'). Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})" - sleep $POLL_INTERVAL - done + wait_until "Storage '$STORAGE_ID' is active." "[ \"\$(pvesm status 2>/dev/null | awk -v id=\"$STORAGE_ID\" '\$1 == id { print \$3; exit }')\" = \"active\" ]" || exit 1 done fi log "All storage dependencies are met." - -# --- 2. Custom Tag-Based Dependency Check --- log "Checking for custom tag-based dependencies..." -TAGS=$(echo "$GUEST_CONFIG" | grep '^tags:' | awk '{print $2}') +TAGS=$(echo "$GUEST_CONFIG" | awk -F': ' '/^tags:/ {print $2}') if [ -z "$TAGS" ]; then log "No tags found. Skipping custom dependency check." else - # Replace colons with spaces to loop through tags for TAG in ${TAGS//;/ }; do - # Check if the tag matches our dependency format 'dep_*' if [[ $TAG == dep_* ]]; then log "Found dependency tag: '$TAG'" - # Split tag into parts using underscore as delimiter - IFS='_' read -ra PARTS <<< "$TAG" - DEP_TYPE="${PARTS[1]}" + IFS='_' read -r _ DEP_TYPE HOST PORT EXTRA <<< "$TAG" - ATTEMPTS=0 - while true; do - CHECK_PASSED=false - case "$DEP_TYPE" in - "tcp") - HOST="${PARTS[2]}" - PORT="${PARTS[3]}" - if [ -z "$HOST" ] || [ -z "$PORT" ]; then - log "ERROR: Malformed TCP dependency tag '$TAG'. Skipping." - CHECK_PASSED=true # Skip to avoid infinite loop - # nc -z is great for this. -w sets a timeout. - elif nc -z -w 2 "$HOST" "$PORT"; then - log "TCP dependency met: Host $HOST port $PORT is open." - CHECK_PASSED=true - fi - ;; - - "ping") - HOST="${PARTS[2]}" - if [ -z "$HOST" ]; then - log "ERROR: Malformed PING dependency tag '$TAG'. Skipping." - CHECK_PASSED=true # Skip to avoid infinite loop - # ping -c 1 (one packet) -W 2 (2-second timeout) - elif ping -c 1 -W 2 "$HOST" >/dev/null 2>&1; then - log "Ping dependency met: Host $HOST is reachable." - CHECK_PASSED=true - fi - ;; - - *) - log "WARNING: Unknown dependency type '$DEP_TYPE' in tag '$TAG'. Ignoring." - CHECK_PASSED=true # Mark as passed to avoid getting stuck - ;; - esac - - if $CHECK_PASSED; then - break - fi - - ATTEMPTS=$((ATTEMPTS + 1)) - if [ $ATTEMPTS -ge $MAX_ATTEMPTS ]; then - log "ERROR: Timeout waiting for dependency '$TAG'. Aborting start." - exit 1 - fi - - log "Dependency '$TAG' not met. Waiting ${POLL_INTERVAL}s... (Attempt ${ATTEMPTS}/${MAX_ATTEMPTS})" - sleep $POLL_INTERVAL - done + case "$DEP_TYPE" in + ping) + if [ -z "$HOST" ]; then + log "WARNING: Malformed ping dependency tag '$TAG'. Ignoring." + continue + fi + wait_until "Ping dependency met: Host $HOST is reachable." "ping -c 1 -W 2 \"$HOST\" >/dev/null 2>&1" || exit 1 + ;; + tcp) + if [ -z "$HOST" ] || [ -z "$PORT" ] || ! [[ "$PORT" =~ ^[0-9]+$ ]] || [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then + log "WARNING: Malformed TCP dependency tag '$TAG'. Expected dep_tcp__. Ignoring." + continue + fi + wait_until "TCP dependency met: Host $HOST port $PORT is open." "check_tcp \"$HOST\" \"$PORT\"" || exit 1 + ;; + *) + log "WARNING: Unknown dependency type '$DEP_TYPE' in tag '$TAG'. Ignoring." + ;; + esac fi done fi @@ -215,17 +239,16 @@ log "All custom dependencies are met." log "--- Dependency Check Complete. Proceeding with start. ---" exit 0 EOF - chmod +x /var/lib/vz/snippets/dependency-check.sh - msg_ok "Created dependency-check hookscript" + chmod +x "$HOOKSCRIPT_FILE" + msg_ok "Created dependency-check hookscript" } -# Function to create the config file for exclusions create_exclusion_config() { - msg_info "Creating exclusion configuration file" - if [ -f /etc/default/pve-auto-hook ]; then - msg_ok "Exclusion file already exists, skipping." - else - cat <<'EOF' > /etc/default/pve-auto-hook + msg_info "Creating exclusion configuration file" + if [ -f "$CONFIG_FILE" ]; then + msg_ok "Exclusion file already exists, skipping." + else + cat <<'EOF' >/etc/default/pve-auto-hook # # Configuration for the Proxmox Automatic Hookscript Applicator # @@ -238,71 +261,99 @@ create_exclusion_config() { IGNORE_IDS="" EOF - msg_ok "Created exclusion configuration file" - fi + chmod 0644 "$CONFIG_FILE" + msg_ok "Created exclusion configuration file" + fi } -# Function to create the script that applies the hook create_applicator_script() { - msg_info "Creating the hookscript applicator script" - cat <<'EOF' > /usr/local/bin/pve-apply-hookscript.sh + msg_info "Creating the hookscript applicator script" + cat <<'EOF' >/usr/local/bin/pve-apply-hookscript.sh #!/bin/bash HOOKSCRIPT_VOLUME_ID="local:snippets/dependency-check.sh" CONFIG_FILE="/etc/default/pve-auto-hook" -LOG_TAG="pve-auto-hook-list" +LOG_TAG="pve-auto-hook" +IGNORE_IDS="" log() { systemd-cat -t "$LOG_TAG" <<< "$1" } if [ -f "$CONFIG_FILE" ]; then - source "$CONFIG_FILE" + IGNORE_IDS=$(grep -E '^IGNORE_IDS=' "$CONFIG_FILE" | head -n1 | cut -d'=' -f2- | tr -d '"') fi -# Process QEMU VMs -qm list | awk 'NR>1 {print $1}' | while read -r VMID; do - is_ignored=false +is_ignored() { + local vmid="$1" for id_to_ignore in $IGNORE_IDS; do - if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi + if [ "$id_to_ignore" = "$vmid" ]; then + return 0 + fi done - if $is_ignored; then continue; fi - if qm config "$VMID" | grep -q '^hookscript:'; then continue; fi - log "Hookscript not found for VM $VMID. Applying..." - qm set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID" -done - -# Process LXC Containers -pct list | awk 'NR>1 {print $1}' | while read -r VMID; do - is_ignored=false - for id_to_ignore in $IGNORE_IDS; do - if [ "$id_to_ignore" == "$VMID" ]; then is_ignored=true; break; fi - done - if $is_ignored; then continue; fi - if pct config "$VMID" | grep -q '^hookscript:'; then continue; fi - log "Hookscript not found for LXC $VMID. Applying..." - pct set "$VMID" --hookscript "$HOOKSCRIPT_VOLUME_ID" -done -EOF - chmod +x /usr/local/bin/pve-apply-hookscript.sh - msg_ok "Created applicator script" + return 1 +} + +ensure_hookscript() { + local guest_type="$1" + local vmid="$2" + local current_hook="" + + if [ "$guest_type" = "qemu" ]; then + current_hook=$(qm config "$vmid" | awk '/^hookscript:/ {print $2}') + else + current_hook=$(pct config "$vmid" | awk '/^hookscript:/ {print $2}') + fi + + if [ -n "$current_hook" ]; then + if [ "$current_hook" = "$HOOKSCRIPT_VOLUME_ID" ]; then + return 0 + fi + log "Guest $guest_type/$vmid already has another hookscript ($current_hook). Leaving unchanged." + return 0 + fi + + log "Applying hookscript to $guest_type/$vmid" + if [ "$guest_type" = "qemu" ]; then + qm set "$vmid" --hookscript "$HOOKSCRIPT_VOLUME_ID" >/dev/null 2>&1 + else + pct set "$vmid" --hookscript "$HOOKSCRIPT_VOLUME_ID" >/dev/null 2>&1 + fi +} + +qm list | awk 'NR>1 {print $1}' | while read -r VMID; do + if is_ignored "$VMID"; then + continue + fi + ensure_hookscript "qemu" "$VMID" +done + +pct list | awk 'NR>1 {print $1}' | while read -r VMID; do + if is_ignored "$VMID"; then + continue + fi + ensure_hookscript "lxc" "$VMID" +done +EOF + chmod +x "$APPLICATOR_FILE" + msg_ok "Created applicator script" } -# Function to set up the systemd watcher and service create_systemd_units() { - msg_info "Creating systemd watcher and service units" - cat <<'EOF' > /etc/systemd/system/pve-auto-hook.path + msg_info "Creating systemd watcher and service units" + cat <<'EOF' >/etc/systemd/system/pve-auto-hook.path [Unit] Description=Watch for new Proxmox guest configs to apply hookscript [Path] -PathModified=/etc/pve/qemu-server/ -PathModified=/etc/pve/lxc/ +PathExistsGlob=/etc/pve/qemu-server/*.conf +PathExistsGlob=/etc/pve/lxc/*.conf +Unit=pve-auto-hook.service [Install] WantedBy=multi-user.target EOF - cat <<'EOF' > /etc/systemd/system/pve-auto-hook.service + cat <<'EOF' >/etc/systemd/system/pve-auto-hook.service [Unit] Description=Automatically add hookscript to new Proxmox guests @@ -310,54 +361,149 @@ Description=Automatically add hookscript to new Proxmox guests Type=oneshot ExecStart=/usr/local/bin/pve-apply-hookscript.sh EOF - msg_ok "Created systemd units" + chmod 0644 "$PATH_UNIT_FILE" "$SERVICE_UNIT_FILE" + msg_ok "Created systemd units" } +remove_hookscript_assignments() { + msg_info "Removing hookscript assignment from guests using dependency-check" + + qm list | awk 'NR>1 {print $1}' | while read -r vmid; do + current_hook=$(qm config "$vmid" | awk '/^hookscript:/ {print $2}') + if [ "$current_hook" = "$HOOKSCRIPT_VOLUME_ID" ]; then + qm set "$vmid" --delete hookscript >/dev/null 2>&1 && msg_ok "Removed hookscript from VM $vmid" + fi + done + + pct list | awk 'NR>1 {print $1}' | while read -r vmid; do + current_hook=$(pct config "$vmid" | awk '/^hookscript:/ {print $2}') + if [ "$current_hook" = "$HOOKSCRIPT_VOLUME_ID" ]; then + pct set "$vmid" --delete hookscript >/dev/null 2>&1 && msg_ok "Removed hookscript from LXC $vmid" + fi + done +} + +install_stack() { + create_dependency_hookscript + create_exclusion_config + create_applicator_script + create_systemd_units + + msg_info "Reloading systemd and enabling watcher" + if systemctl daemon-reload && systemctl enable --now pve-auto-hook.path >/dev/null 2>&1; then + msg_ok "Systemd watcher enabled and running" + else + msg_error "Could not enable pve-auto-hook.path" + exit 1 + fi + + msg_info "Performing initial run to update existing guests" + if "$APPLICATOR_FILE" >/dev/null 2>&1; then + msg_ok "Initial run complete" + else + msg_error "Initial run failed" + exit 1 + fi +} + +uninstall_stack() { + remove_hookscript_assignments + + msg_info "Stopping and disabling systemd units" + systemctl disable --now pve-auto-hook.path >/dev/null 2>&1 || true + systemctl disable --now pve-auto-hook.service >/dev/null 2>&1 || true + + msg_info "Removing installed files" + rm -f "$HOOKSCRIPT_FILE" "$APPLICATOR_FILE" "$PATH_UNIT_FILE" "$SERVICE_UNIT_FILE" "$CONFIG_FILE" + + if systemctl daemon-reload >/dev/null 2>&1; then + msg_ok "systemd daemon reloaded" + else + msg_error "Failed to reload systemd daemon" + exit 1 + fi + + msg_ok "Dependency-check stack successfully removed" +} + +show_status() { + echo -e "\n${BL}Dependency-check status${CL}" + echo -e "--------------------------------" + [ -f "$HOOKSCRIPT_FILE" ] && echo -e "Hookscript file: ${GN}present${CL}" || echo -e "Hookscript file: ${RD}missing${CL}" + [ -f "$APPLICATOR_FILE" ] && echo -e "Applicator script: ${GN}present${CL}" || echo -e "Applicator script: ${RD}missing${CL}" + [ -f "$CONFIG_FILE" ] && echo -e "Config file: ${GN}present${CL}" || echo -e "Config file: ${RD}missing${CL}" + [ -f "$PATH_UNIT_FILE" ] && echo -e "Path unit: ${GN}present${CL}" || echo -e "Path unit: ${RD}missing${CL}" + [ -f "$SERVICE_UNIT_FILE" ] && echo -e "Service unit: ${GN}present${CL}" || echo -e "Service unit: ${RD}missing${CL}" + + if systemctl is-enabled pve-auto-hook.path >/dev/null 2>&1; then + echo -e "Watcher enabled: ${GN}yes${CL}" + else + echo -e "Watcher enabled: ${YW}no${CL}" + fi + + if systemctl is-active pve-auto-hook.path >/dev/null 2>&1; then + echo -e "Watcher active: ${GN}yes${CL}" + else + echo -e "Watcher active: ${YW}no${CL}" + fi +} -# --- Main Execution --- header_info +ensure_supported_pve -if ! command -v pveversion >/dev/null 2>&1; then - msg_error "This script must be run on a Proxmox VE host." +case "${1:---install}" in +--help | -h) + print_usage + exit 0 + ;; +--status) + show_status + exit 0 + ;; +--install) + echo -e "\nThis script will install a service to automatically apply a" + echo -e "dependency-checking hookscript to all new and existing Proxmox guests." + echo -e "${YW}This includes creating files in:${CL}" + echo -e " - /var/lib/vz/snippets/" + echo -e " - /usr/local/bin/" + echo -e " - /etc/default/" + echo -e " - /etc/systemd/system/\n" + + if ! confirm_action "Do you want to proceed with the installation?"; then + msg_error "Installation cancelled" exit 1 -fi + fi -echo -e "\nThis script will install a service to automatically apply a" -echo -e "dependency-checking hookscript to all new and existing Proxmox guests." -echo -e "${YW}This includes creating files in:${CL}" -echo -e " - /var/lib/vz/snippets/" -echo -e " - /usr/local/bin/" -echo -e " - /etc/default/" -echo -e " - /etc/systemd/system/\n" + echo "" + install_stack -read -p "Do you want to proceed with the installation? (y/n): " -n 1 -r -echo -if [[ ! $REPLY =~ ^[Yy]$ ]]; then - msg_error "Installation cancelled." + echo -e "\n${GN}Installation successful!${CL}" + echo -e "The service is now active and will monitor for new guests." + echo -e "To ${YW}exclude${CL} a VM or LXC, add its ID to ${YW}IGNORE_IDS${CL} in:" + echo -e " ${YW}${CONFIG_FILE}${CL}" + echo -e "\nMonitor activity with:" + echo -e " ${YW}journalctl -fu pve-auto-hook.service${CL}\n" + ;; +--uninstall) + echo -e "\nThis will completely remove the dependency-check stack:" + echo -e " - hookscript and applicator" + echo -e " - systemd path/service units" + echo -e " - exclusion config" + echo -e " - hookscript assignment from guests using ${HOOKSCRIPT_VOLUME_ID}\n" + + if ! confirm_action "Do you want to proceed with uninstall?"; then + msg_error "Uninstall cancelled" exit 1 -fi + fi -echo -e "\n" -create_dependency_hookscript -create_exclusion_config -create_applicator_script -create_systemd_units - -msg_info "Reloading systemd and enabling the watcher" -(systemctl daemon-reload && systemctl enable --now pve-auto-hook.path) >/dev/null 2>&1 & -spinner -msg_ok "Systemd watcher enabled and running" - -msg_info "Performing initial run to update existing guests" -/usr/local/bin/pve-apply-hookscript.sh >/dev/null 2>&1 & -spinner -msg_ok "Initial run complete" - -echo -e "\n\n${GN}Installation successful!${CL}" -echo -e "The service is now active and will monitor for new guests." -echo -e "To ${YW}exclude${CL} a VM or LXC, add its ID to the ${YW}IGNORE_IDS${CL} variable in:" -echo -e " ${YW}/etc/default/pve-auto-hook${CL}" -echo -e "\nYou can monitor the service's activity with:" -echo -e " ${YW}journalctl -fu pve-auto-hook.service${CL}\n" + echo "" + uninstall_stack + ;; +*) + msg_error "Unknown option: $1" + print_usage + exit 1 + ;; +esac exit 0 From f1da768e02611b525ac9ece6e0a41d170fd1eddd Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:24:47 +0100 Subject: [PATCH 05/19] Use pushd/popd and set ALEMBIC_CONFIG Replace plain cd with pushd/popd (silenced) when building the frontend and running migrations to preserve the original working directory. Ensure database migrations run from /opt/gramps-web-api and export ALEMBIC_CONFIG in both install and update scripts so Alembic uses the correct config. Minor cleanup to silence directory stack output. --- ct/gramps-web.sh | 6 +++++- install/gramps-web-install.sh | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/ct/gramps-web.sh b/ct/gramps-web.sh index fe3d29ece..19f8a0858 100644 --- a/ct/gramps-web.sh +++ b/ct/gramps-web.sh @@ -85,18 +85,22 @@ function update_script() { msg_ok "Updated Gramps Web API" msg_info "Updating Gramps Web Frontend" - cd /opt/gramps-web/frontend + pushd /opt/gramps-web/frontend >/dev/null export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack enable $STD npm install $STD npm run build + popd >/dev/null msg_ok "Updated Gramps Web Frontend" msg_info "Applying Database Migration" + pushd /opt/gramps-web-api >/dev/null GRAMPS_API_CONFIG=/opt/gramps-web/config/config.cfg \ + ALEMBIC_CONFIG=/opt/gramps-web-api/alembic.ini \ GRAMPSHOME=/opt/gramps-web/data/gramps \ GRAMPS_DATABASE_PATH=/opt/gramps-web/data/gramps/grampsdb \ $STD /opt/gramps-web/venv/bin/python3 -m gramps_webapi user migrate + popd >/dev/null msg_ok "Applied Database Migration" msg_info "Starting Service" diff --git a/install/gramps-web-install.sh b/install/gramps-web-install.sh index dde886341..0014d670b 100644 --- a/install/gramps-web-install.sh +++ b/install/gramps-web-install.sh @@ -88,16 +88,20 @@ $STD uv pip install --no-cache-dir --upgrade pip setuptools wheel $STD uv pip install --no-cache-dir gunicorn $STD uv pip install --no-cache-dir /opt/gramps-web-api -cd /opt/gramps-web/frontend +pushd /opt/gramps-web/frontend >/dev/null export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack enable $STD npm install $STD npm run build +popd >/dev/null +pushd /opt/gramps-web-api >/dev/null GRAMPS_API_CONFIG=/opt/gramps-web/config/config.cfg \ + ALEMBIC_CONFIG=/opt/gramps-web-api/alembic.ini \ GRAMPSHOME=/opt/gramps-web/data/gramps \ GRAMPS_DATABASE_PATH=/opt/gramps-web/data/gramps/grampsdb \ $STD /opt/gramps-web/venv/bin/python3 -m gramps_webapi user migrate +popd >/dev/null msg_ok "Set up Gramps Web" msg_info "Creating Service" From a1bcf8eb04be63f6f29b600d8296376e29ca0a0c Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:38:03 +0100 Subject: [PATCH 06/19] Replace pushd/popd with cd in scripts Replace pushd/popd directory stack usage with plain cd in ct/gramps-web.sh and install/gramps-web-install.sh, and remove the >/dev/null redirections. The frontend build and backend migration steps remain functionally the same but use simpler directory changes to avoid relying on pushd/popd output suppression. --- ct/gramps-web.sh | 6 ++---- install/gramps-web-install.sh | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/ct/gramps-web.sh b/ct/gramps-web.sh index 19f8a0858..a90a80b82 100644 --- a/ct/gramps-web.sh +++ b/ct/gramps-web.sh @@ -85,22 +85,20 @@ function update_script() { msg_ok "Updated Gramps Web API" msg_info "Updating Gramps Web Frontend" - pushd /opt/gramps-web/frontend >/dev/null + cd /opt/gramps-web/frontend export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack enable $STD npm install $STD npm run build - popd >/dev/null msg_ok "Updated Gramps Web Frontend" msg_info "Applying Database Migration" - pushd /opt/gramps-web-api >/dev/null + cd /opt/gramps-web-api GRAMPS_API_CONFIG=/opt/gramps-web/config/config.cfg \ ALEMBIC_CONFIG=/opt/gramps-web-api/alembic.ini \ GRAMPSHOME=/opt/gramps-web/data/gramps \ GRAMPS_DATABASE_PATH=/opt/gramps-web/data/gramps/grampsdb \ $STD /opt/gramps-web/venv/bin/python3 -m gramps_webapi user migrate - popd >/dev/null msg_ok "Applied Database Migration" msg_info "Starting Service" diff --git a/install/gramps-web-install.sh b/install/gramps-web-install.sh index 0014d670b..5a64cf5ac 100644 --- a/install/gramps-web-install.sh +++ b/install/gramps-web-install.sh @@ -88,20 +88,18 @@ $STD uv pip install --no-cache-dir --upgrade pip setuptools wheel $STD uv pip install --no-cache-dir gunicorn $STD uv pip install --no-cache-dir /opt/gramps-web-api -pushd /opt/gramps-web/frontend >/dev/null +cd /opt/gramps-web/frontend export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 corepack enable $STD npm install $STD npm run build -popd >/dev/null -pushd /opt/gramps-web-api >/dev/null +cd /opt/gramps-web-api GRAMPS_API_CONFIG=/opt/gramps-web/config/config.cfg \ ALEMBIC_CONFIG=/opt/gramps-web-api/alembic.ini \ GRAMPSHOME=/opt/gramps-web/data/gramps \ GRAMPS_DATABASE_PATH=/opt/gramps-web/data/gramps/grampsdb \ $STD /opt/gramps-web/venv/bin/python3 -m gramps_webapi user migrate -popd >/dev/null msg_ok "Set up Gramps Web" msg_info "Creating Service" From f6787f7641146268eb7aa08e09fb6bd3bc1e8d2a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 09:41:28 +0000 Subject: [PATCH 07/19] Delete databasus (ct) after migration to ProxmoxVE (#1471) Co-authored-by: github-actions[bot] --- ct/databasus.sh | 80 ------------- frontend/public/json/databasus.json | 44 ------- install/databasus-install.sh | 171 ---------------------------- 3 files changed, 295 deletions(-) delete mode 100644 ct/databasus.sh delete mode 100644 frontend/public/json/databasus.json delete mode 100644 install/databasus-install.sh diff --git a/ct/databasus.sh b/ct/databasus.sh deleted file mode 100644 index c0eab526b..000000000 --- a/ct/databasus.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVED/main/misc/build.func) -# Copyright (c) 2021-2026 community-scripts ORG -# Author: MickLesk (CanbiZ) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/databasus/databasus - -APP="Databasus" -var_tags="${var_tags:-backup;postgresql;database}" -var_cpu="${var_cpu:-2}" -var_ram="${var_ram:-2048}" -var_disk="${var_disk:-8}" -var_os="${var_os:-debian}" -var_version="${var_version:-13}" -var_unprivileged="${var_unprivileged:-1}" - -header_info "$APP" -variables -color -catch_errors - -function update_script() { - header_info - check_container_storage - check_container_resources - - if [[ ! -f /opt/databasus/databasus ]]; then - msg_error "No ${APP} Installation Found!" - exit - fi - - if check_for_gh_release "databasus" "databasus/databasus"; then - msg_info "Stopping Databasus" - $STD systemctl stop databasus - msg_ok "Stopped Databasus" - - msg_info "Backing up Configuration" - cp /opt/databasus/.env /tmp/databasus.env.bak - msg_ok "Backed up Configuration" - - msg_info "Updating Databasus" - fetch_and_deploy_gh_release "databasus" "databasus/databasus" "tarball" "latest" "/opt/databasus" - - cd /opt/databasus/frontend - $STD npm ci - $STD npm run build - - cd /opt/databasus/backend - $STD go mod download - $STD /root/go/bin/swag init -g cmd/main.go -o swagger - $STD env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o databasus ./cmd/main.go - mv /opt/databasus/backend/databasus /opt/databasus/databasus - - cp -r /opt/databasus/frontend/dist/* /opt/databasus/ui/build/ - cp -r /opt/databasus/backend/migrations /opt/databasus/ - chown -R postgres:postgres /opt/databasus - msg_ok "Updated Databasus" - - msg_info "Restoring Configuration" - cp /tmp/databasus.env.bak /opt/databasus/.env - rm -f /tmp/databasus.env.bak - chown postgres:postgres /opt/databasus/.env - msg_ok "Restored Configuration" - - msg_info "Starting Databasus" - $STD systemctl start databasus - msg_ok "Started Databasus" - msg_ok "Updated successfully!" - fi - exit -} - -start -build_container -description - -msg_ok "Completed successfully!\n" -echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" -echo -e "${INFO}${YW} Access it using the following URL:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}" diff --git a/frontend/public/json/databasus.json b/frontend/public/json/databasus.json deleted file mode 100644 index 766d88925..000000000 --- a/frontend/public/json/databasus.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "Databasus", - "slug": "databasus", - "categories": [ - 7 - ], - "date_created": "2025-01-14", - "type": "ct", - "updateable": true, - "privileged": false, - "interface_port": 80, - "documentation": "https://github.com/databasus/databasus", - "website": "https://github.com/databasus/databasus", - "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/databasus.webp", - "config_path": "/opt/databasus/.env", - "description": "Free, open source and self-hosted solution for automated PostgreSQL backups. With multiple storage options, notifications, scheduling, and a beautiful web interface for managing database backups across multiple PostgreSQL instances.", - "install_methods": [ - { - "type": "default", - "script": "ct/databasus.sh", - "resources": { - "cpu": 2, - "ram": 2048, - "hdd": 8, - "os": "Debian", - "version": "13" - } - } - ], - "default_credentials": { - "username": "admin@localhost", - "password": "See /root/databasus.creds" - }, - "notes": [ - { - "text": "Supports PostgreSQL versions 12-18 with cloud and self-hosted instances", - "type": "info" - }, - { - "text": "Features: Scheduled backups, multiple storage providers, notifications, encryption", - "type": "info" - } - ] -} diff --git a/install/databasus-install.sh b/install/databasus-install.sh deleted file mode 100644 index b6001ab2a..000000000 --- a/install/databasus-install.sh +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2021-2026 community-scripts ORG -# Author: MickLesk (CanbiZ) -# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE -# Source: https://github.com/databasus/databasus - -source /dev/stdin <<<"$FUNCTIONS_FILE_PATH" -color -verb_ip6 -catch_errors -setting_up_container -network_check -update_os - -msg_info "Installing Dependencies" -$STD apt install -y \ - nginx \ - valkey -msg_ok "Installed Dependencies" - -PG_VERSION="17" setup_postgresql -setup_go -NODE_VERSION="24" setup_nodejs - -fetch_and_deploy_gh_release "databasus" "databasus/databasus" "tarball" "latest" "/opt/databasus" - -msg_info "Building Databasus (Patience)" -cd /opt/databasus/frontend -$STD npm ci -$STD npm run build -cd /opt/databasus/backend -$STD go mod tidy -$STD go mod download -$STD go install github.com/swaggo/swag/cmd/swag@latest -$STD /root/go/bin/swag init -g cmd/main.go -o swagger -$STD env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o databasus ./cmd/main.go -mv /opt/databasus/backend/databasus /opt/databasus/databasus -mkdir -p /databasus-data/{pgdata,temp,backups,data,logs} -mkdir -p /opt/databasus/ui/build -mkdir -p /opt/databasus/migrations -cp -r /opt/databasus/frontend/dist/* /opt/databasus/ui/build/ -cp -r /opt/databasus/backend/migrations/* /opt/databasus/migrations/ -chown -R postgres:postgres /databasus-data -msg_ok "Built Databasus" - -msg_info "Configuring Databasus" -JWT_SECRET=$(openssl rand -hex 32) -ENCRYPTION_KEY=$(openssl rand -hex 32) -# Create PostgreSQL version symlinks for compatibility -for v in 12 13 14 15 16 18; do - ln -sf /usr/lib/postgresql/17 /usr/lib/postgresql/$v -done -# Install goose for migrations -$STD go install github.com/pressly/goose/v3/cmd/goose@latest -ln -sf /root/go/bin/goose /usr/local/bin/goose -cat </opt/databasus/.env -# Environment -ENV_MODE=production - -# Server -SERVER_PORT=4005 -SERVER_HOST=0.0.0.0 - -# Database -DATABASE_DSN=host=localhost user=postgres password=postgres dbname=databasus port=5432 sslmode=disable -DATABASE_URL=postgres://postgres:postgres@localhost:5432/databasus?sslmode=disable - -# Migrations -GOOSE_DRIVER=postgres -GOOSE_DBSTRING=postgres://postgres:postgres@localhost:5432/databasus?sslmode=disable -GOOSE_MIGRATION_DIR=/opt/databasus/migrations - -# Valkey (Redis-compatible cache) -VALKEY_HOST=localhost -VALKEY_PORT=6379 - -# Security -JWT_SECRET=${JWT_SECRET} -ENCRYPTION_KEY=${ENCRYPTION_KEY} - -# Paths -DATA_DIR=/databasus-data/data -BACKUP_DIR=/databasus-data/backups -LOG_DIR=/databasus-data/logs -EOF -chown postgres:postgres /opt/databasus/.env -chmod 600 /opt/databasus/.env -msg_ok "Configured Databasus" - -msg_info "Configuring Valkey" -cat >/etc/valkey/valkey.conf </dev/null || true -$STD sudo -u postgres psql -c "ALTER USER postgres WITH SUPERUSER CREATEROLE CREATEDB;" 2>/dev/null || true -msg_ok "Created Database" - -msg_info "Creating Databasus Service" -cat </etc/systemd/system/databasus.service -[Unit] -Description=Databasus - Database Backup Management -After=network.target postgresql.service valkey.service -Requires=postgresql.service valkey.service - -[Service] -Type=simple -WorkingDirectory=/opt/databasus -EnvironmentFile=/opt/databasus/.env -ExecStart=/opt/databasus/databasus -Restart=always -RestartSec=5 -StandardOutput=journal -StandardError=journal - -[Install] -WantedBy=multi-user.target -EOF -$STD systemctl daemon-reload -$STD systemctl enable -q --now databasus -msg_ok "Created Databasus Service" - -msg_info "Configuring Nginx" -cat </etc/nginx/sites-available/databasus -server { - listen 80; - server_name _; - - location / { - proxy_pass http://127.0.0.1:4005; - proxy_http_version 1.1; - proxy_set_header Upgrade \$http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host \$host; - proxy_set_header X-Real-IP \$remote_addr; - proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto \$scheme; - proxy_cache_bypass \$http_upgrade; - proxy_buffering off; - proxy_read_timeout 86400s; - proxy_send_timeout 86400s; - } -} -EOF -ln -sf /etc/nginx/sites-available/databasus /etc/nginx/sites-enabled/databasus -rm -f /etc/nginx/sites-enabled/default -$STD nginx -t -$STD systemctl enable -q --now nginx -$STD systemctl reload nginx -msg_ok "Configured Nginx" - -motd_ssh -customize -cleanup_lxc From 76eb716da714a42b91ec4a622e2341149e7e02f7 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:57:21 +0100 Subject: [PATCH 08/19] Retry dependency install without build isolation Add retry logic to ct/calibre-web.sh and install/calibre-web-install.sh: if 'uv sync --no-dev' fails, the scripts now create/ensure a venv, install the 'calibreweb' package into it via 'uv pip', and retry 'uv sync' with '--no-build-isolation'. This works around failures caused by isolated builds or build-step issues and preserves existing log messages. --- ct/calibre-web.sh | 7 ++++++- install/calibre-web-install.sh | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ct/calibre-web.sh b/ct/calibre-web.sh index 278dd9ff8..63a5c1221 100644 --- a/ct/calibre-web.sh +++ b/ct/calibre-web.sh @@ -43,7 +43,12 @@ function update_script() { msg_info "Installing Dependencies" cd /opt/calibre-web - $STD uv sync --no-dev + if ! $STD uv sync --no-dev; then + msg_info "Retrying dependency install without build isolation" + $STD uv venv + $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir calibreweb + $STD uv sync --no-dev --no-build-isolation + fi msg_ok "Installed Dependencies" msg_info "Restoring Data" diff --git a/install/calibre-web-install.sh b/install/calibre-web-install.sh index dc201423c..566e50069 100644 --- a/install/calibre-web-install.sh +++ b/install/calibre-web-install.sh @@ -32,7 +32,12 @@ setup_uv msg_info "Installing Python Dependencies" cd /opt/calibre-web -$STD uv sync --no-dev +if ! $STD uv sync --no-dev; then + msg_info "Retrying Python dependency install without build isolation" + $STD uv venv + $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir calibreweb + $STD uv sync --no-dev --no-build-isolation +fi msg_ok "Installed Python Dependencies" msg_info "Creating Service" From a12bfd9ab38282b10ed30c8e24ff7fffde51da18 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:12:23 +0100 Subject: [PATCH 09/19] Unconditionally create venv and install deps Remove the conditional retry logic and always run the venv creation, pip install of calibreweb, and uv sync without build isolation. Updated ct/calibre-web.sh and install/calibre-web-install.sh to: run `$STD uv venv`, `$STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir calibreweb`, then `$STD uv sync --no-dev --no-build-isolation`. This simplifies the dependency installation flow and ensures the virtualenv and package are installed before syncing. --- ct/calibre-web.sh | 9 +++------ install/calibre-web-install.sh | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/ct/calibre-web.sh b/ct/calibre-web.sh index 63a5c1221..d16139d35 100644 --- a/ct/calibre-web.sh +++ b/ct/calibre-web.sh @@ -43,12 +43,9 @@ function update_script() { msg_info "Installing Dependencies" cd /opt/calibre-web - if ! $STD uv sync --no-dev; then - msg_info "Retrying dependency install without build isolation" - $STD uv venv - $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir calibreweb - $STD uv sync --no-dev --no-build-isolation - fi + $STD uv venv + $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir calibreweb + $STD uv sync --no-dev --no-build-isolation msg_ok "Installed Dependencies" msg_info "Restoring Data" diff --git a/install/calibre-web-install.sh b/install/calibre-web-install.sh index 566e50069..41e971332 100644 --- a/install/calibre-web-install.sh +++ b/install/calibre-web-install.sh @@ -32,12 +32,9 @@ setup_uv msg_info "Installing Python Dependencies" cd /opt/calibre-web -if ! $STD uv sync --no-dev; then - msg_info "Retrying Python dependency install without build isolation" - $STD uv venv - $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir calibreweb - $STD uv sync --no-dev --no-build-isolation -fi +$STD uv venv +$STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir calibreweb +$STD uv sync --no-dev --no-build-isolation msg_ok "Installed Python Dependencies" msg_info "Creating Service" From dc8d48128c82284de752a8726a74f27f1e395a82 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:13:48 +0100 Subject: [PATCH 10/19] add pgvector --- ct/discourse.sh | 1 + install/discourse-install.sh | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ct/discourse.sh b/ct/discourse.sh index 044ef5f48..cf4f90fea 100644 --- a/ct/discourse.sh +++ b/ct/discourse.sh @@ -43,6 +43,7 @@ function update_script() { msg_ok "Backed up Data" msg_info "Updating Discourse" + PG_VERSION="16" PG_MODULES="pgvector" setup_postgresql cd /opt/discourse git pull origin main $STD bundle install --deployment --without test development diff --git a/install/discourse-install.sh b/install/discourse-install.sh index aa3e575d6..8eef44ef6 100644 --- a/install/discourse-install.sh +++ b/install/discourse-install.sh @@ -28,7 +28,7 @@ $STD apt install -y \ redis-server msg_ok "Installed Dependencies" -PG_VERSION="16" setup_postgresql +PG_VERSION="16" PG_MODULES="pgvector" setup_postgresql NODE_VERSION="22" setup_nodejs RUBY_VERSION="3.4.4" setup_ruby From c0f768e188fa50653af176b27b85a3e2ffd92f4f Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:25:01 +0100 Subject: [PATCH 11/19] vector --- ct/discourse.sh | 1 + install/discourse-install.sh | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/ct/discourse.sh b/ct/discourse.sh index cf4f90fea..e17884c8a 100644 --- a/ct/discourse.sh +++ b/ct/discourse.sh @@ -48,6 +48,7 @@ function update_script() { git pull origin main $STD bundle install --deployment --without test development $STD yarn install + $STD runuser -u postgres -- psql -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;" $STD bundle exec rails assets:precompile $STD bundle exec rails db:migrate msg_ok "Updated Discourse" diff --git a/install/discourse-install.sh b/install/discourse-install.sh index 8eef44ef6..36385fa33 100644 --- a/install/discourse-install.sh +++ b/install/discourse-install.sh @@ -34,20 +34,16 @@ RUBY_VERSION="3.4.4" setup_ruby msg_info "Configuring PostgreSQL for Discourse" DISCOURSE_DB_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c13) -# Configure pg_hba.conf for md5 authentication PG_HBA="/etc/postgresql/16/main/pg_hba.conf" sed -i 's/^local\s\+all\s\+all\s\+peer$/local all all md5/' "$PG_HBA" $STD systemctl restart postgresql -# Create user + database explicitly for reliable bootstrap PG_DB_NAME="discourse" PG_DB_USER="discourse" PG_DB_PASS="$DISCOURSE_DB_PASS" setup_postgresql_db msg_ok "Configured PostgreSQL for Discourse" msg_info "Configuring Discourse" DISCOURSE_SECRET_KEY=$(openssl rand -hex 64) - -git clone --depth 1 https://github.com/discourse/discourse.git /opt/discourse +$STD git clone --depth 1 https://github.com/discourse/discourse.git /opt/discourse cd /opt/discourse - cat </opt/discourse/.env RAILS_ENV=production RAILS_LOG_TO_STDOUT=true @@ -93,6 +89,7 @@ export RAILS_ENV=production set -a source /opt/discourse/.env set +a +$STD runuser -u postgres -- psql -d discourse -c "CREATE EXTENSION IF NOT EXISTS vector;" $STD bundle exec rails db:migrate msg_ok "Set Up Database" From 792f03aeede04eb8cc022b6da2cd7734ee14056a Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:26:06 +0100 Subject: [PATCH 12/19] update api from live --- misc/api.func | 151 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 112 insertions(+), 39 deletions(-) diff --git a/misc/api.func b/misc/api.func index 4a554ecfc..21aa9b057 100644 --- a/misc/api.func +++ b/misc/api.func @@ -91,7 +91,7 @@ detect_repo_source() { community-scripts/ProxmoxVED) REPO_SOURCE="ProxmoxVED" ;; "") # No URL detected — use hardcoded fallback - # CI sed transforms this on promotion: ProxmoxVED → ProxmoxVE + # This value must match the repo: ProxmoxVE for production, ProxmoxVED for dev REPO_SOURCE="ProxmoxVED" ;; *) @@ -135,19 +135,44 @@ explain_exit_code() { # --- Generic / Shell --- 1) echo "General error / Operation not permitted" ;; 2) echo "Misuse of shell builtins (e.g. syntax error)" ;; + 10) echo "Docker / privileged mode required (unsupported environment)" ;; # --- curl / wget errors (commonly seen in downloads) --- + 4) echo "curl: Feature not supported or protocol error" ;; + 5) echo "curl: Could not resolve proxy" ;; 6) echo "curl: DNS resolution failed (could not resolve host)" ;; 7) echo "curl: Failed to connect (network unreachable / host down)" ;; + 8) echo "curl: FTP server reply error" ;; 22) echo "curl: HTTP error returned (404, 429, 500+)" ;; + 23) echo "curl: Write error (disk full or permissions)" ;; + 25) echo "curl: Upload failed" ;; 28) echo "curl: Operation timeout (network slow or server not responding)" ;; + 30) echo "curl: FTP port command failed" ;; 35) echo "curl: SSL/TLS handshake failed (certificate error)" ;; + 56) echo "curl: Receive error (connection reset by peer)" ;; + 75) echo "Temporary failure (retry later)" ;; + 78) echo "curl: Remote file not found (404 on FTP/file)" ;; # --- Package manager / APT / DPKG --- 100) echo "APT: Package manager error (broken packages / dependency problems)" ;; 101) echo "APT: Configuration error (bad sources.list, malformed config)" ;; 102) echo "APT: Lock held by another process (dpkg/apt still running)" ;; + # --- BSD sysexits.h (64-78) --- + 64) echo "Usage error (wrong arguments)" ;; + 65) echo "Data format error (bad input data)" ;; + 66) echo "Input file not found (cannot open input)" ;; + 67) echo "User not found (addressee unknown)" ;; + 68) echo "Host not found (hostname unknown)" ;; + 69) echo "Service unavailable" ;; + 70) echo "Internal software error" ;; + 71) echo "System error (OS-level failure)" ;; + 72) echo "Critical OS file missing" ;; + 73) echo "Cannot create output file" ;; + 74) echo "I/O error" ;; + 76) echo "Remote protocol error" ;; + 77) echo "Permission denied" ;; + # --- Common shell/system errors --- 124) echo "Command timed out (timeout command)" ;; 126) echo "Command invoked cannot execute (permission problem?)" ;; @@ -237,16 +262,21 @@ explain_exit_code() { # json_escape() # # - Escapes a string for safe JSON embedding +# - Strips ANSI escape sequences and non-printable control characters # - Handles backslashes, quotes, newlines, tabs, and carriage returns # ------------------------------------------------------------------------------ json_escape() { local s="$1" + # Strip ANSI escape sequences (color codes etc.) + s=$(printf '%s' "$s" | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g') s=${s//\\/\\\\} s=${s//"/\\"/} s=${s//$'\n'/\\n} s=${s//$'\r'/} s=${s//$'\t'/\\t} - echo "$s" + # Remove any remaining control characters (0x00-0x1F except those already handled) + s=$(printf '%s' "$s" | tr -d '\000-\010\013\014\016-\037') + printf '%s' "$s" } # ------------------------------------------------------------------------------ @@ -283,7 +313,33 @@ get_error_text() { fi if [[ -n "$logfile" && -s "$logfile" ]]; then - tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//' + tail -n 20 "$logfile" 2>/dev/null | sed 's/\r$//' | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' + fi +} + +# ------------------------------------------------------------------------------ +# build_error_string() +# +# - Builds a structured error string for telemetry reporting +# - Format: "exit_code= | \n---\n" +# - If no log lines available, returns just the explanation +# - Arguments: +# * $1: exit_code (numeric) +# * $2: log_text (optional, output from get_error_text) +# - Returns structured error string via stdout +# ------------------------------------------------------------------------------ +build_error_string() { + local exit_code="${1:-1}" + local log_text="${2:-}" + local explanation + explanation=$(explain_exit_code "$exit_code") + + if [[ -n "$log_text" ]]; then + # Structured format: header + separator + log lines + printf 'exit_code=%s | %s\n---\n%s' "$exit_code" "$explanation" "$log_text" + else + # No log available - just the explanation with exit code + printf 'exit_code=%s | %s' "$exit_code" "$explanation" fi } @@ -369,18 +425,19 @@ detect_cpu() { # - Detects RAM speed using dmidecode # - Sets RAM_SPEED global (e.g., "4800" for DDR5-4800) # - Requires root access for dmidecode -# - Returns empty if not available +# - Returns empty if not available or if speed is "Unknown" (nested VMs) # ------------------------------------------------------------------------------ detect_ram() { RAM_SPEED="" if command -v dmidecode &>/dev/null; then # Get configured memory speed (actual running speed) - RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Configured Memory Speed:" | grep -oE "[0-9]+" | head -1) + # Use || true to handle "Unknown" values in nested VMs (no numeric match) + RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Configured Memory Speed:" | grep -oE "[0-9]+" | head -1) || true # Fallback to Speed: if Configured not available if [[ -z "$RAM_SPEED" ]]; then - RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Speed:" | grep -oE "[0-9]+" | head -1) + RAM_SPEED=$(dmidecode -t memory 2>/dev/null | grep -m1 "Speed:" | grep -oE "[0-9]+" | head -1) || true fi fi @@ -592,6 +649,8 @@ EOF curl -fsS -m "${TELEMETRY_TIMEOUT}" -X POST "${TELEMETRY_URL}" \ -H "Content-Type: application/json" \ -d "$JSON_PAYLOAD" &>/dev/null || true + + POST_TO_API_DONE=true } # ------------------------------------------------------------------------------ @@ -665,13 +724,12 @@ post_update_to_api() { else exit_code=1 fi + # Get log lines and build structured error string local error_text="" error_text=$(get_error_text) - if [[ -n "$error_text" ]]; then - error=$(json_escape "$error_text") - else - error=$(json_escape "$(explain_exit_code "$exit_code")") - fi + local full_error + full_error=$(build_error_string "$exit_code" "$error_text") + error=$(json_escape "$full_error") short_error=$(json_escape "$(explain_exit_code "$exit_code")") error_category=$(categorize_error "$exit_code") [[ -z "$error" ]] && error="Unknown error" @@ -814,31 +872,52 @@ EOF categorize_error() { local code="$1" case "$code" in - # Network errors - 6 | 7 | 22 | 28 | 35) echo "network" ;; + # Network errors (curl/wget) + 6 | 7 | 22 | 35) echo "network" ;; - # Storage errors - 214 | 217 | 219) echo "storage" ;; + # Timeout errors + 28 | 124 | 211) echo "timeout" ;; - # Dependency/Package errors - 100 | 101 | 102 | 127 | 160 | 161 | 162) echo "dependency" ;; + # Storage errors (Proxmox storage) + 214 | 217 | 219 | 224) echo "storage" ;; + + # Dependency/Package errors (APT, DPKG, pip, commands) + 100 | 101 | 102 | 127 | 160 | 161 | 162 | 255) echo "dependency" ;; # Permission errors 126 | 152) echo "permission" ;; - # Timeout errors - 124 | 28 | 211) echo "timeout" ;; + # Configuration errors (Proxmox config, invalid args) + 128 | 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;; - # Configuration errors - 203 | 204 | 205 | 206 | 207 | 208) echo "config" ;; + # Proxmox container/template errors + 200 | 209 | 210 | 212 | 213 | 215 | 216 | 218 | 220 | 221 | 222 | 223 | 225 | 231) echo "proxmox" ;; + + # Service/Systemd errors + 150 | 151 | 153 | 154) echo "service" ;; + + # Database errors (PostgreSQL, MySQL, MongoDB) + 170 | 171 | 172 | 173 | 180 | 181 | 182 | 183 | 190 | 191 | 192 | 193) echo "database" ;; + + # Node.js / JavaScript runtime errors + 243 | 245 | 246 | 247 | 248 | 249) echo "runtime" ;; + + # Python environment errors + # (already covered: 160-162 under dependency) # Aborted by user 130) echo "aborted" ;; - # Resource errors (OOM, etc) - 137 | 134) echo "resource" ;; + # Resource errors (OOM, SIGKILL, SIGABRT) + 134 | 137) echo "resource" ;; - # Default + # Signal/Process errors (SIGTERM, SIGPIPE, SIGSEGV) + 139 | 141 | 143) echo "signal" ;; + + # Shell errors (general error, syntax error) + 1 | 2) echo "shell" ;; + + # Default - truly unknown *) echo "unknown" ;; esac } @@ -901,11 +980,9 @@ post_tool_to_api() { [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1 local error_text="" error_text=$(get_error_text) - if [[ -n "$error_text" ]]; then - error=$(json_escape "$error_text") - else - error=$(json_escape "$(explain_exit_code "$exit_code")") - fi + local full_error + full_error=$(build_error_string "$exit_code" "$error_text") + error=$(json_escape "$full_error") error_category=$(categorize_error "$exit_code") fi @@ -968,11 +1045,9 @@ post_addon_to_api() { [[ ! "$exit_code" =~ ^[0-9]+$ ]] && exit_code=1 local error_text="" error_text=$(get_error_text) - if [[ -n "$error_text" ]]; then - error=$(json_escape "$error_text") - else - error=$(json_escape "$(explain_exit_code "$exit_code")") - fi + local full_error + full_error=$(build_error_string "$exit_code" "$error_text") + error=$(json_escape "$full_error") error_category=$(categorize_error "$exit_code") fi @@ -1067,11 +1142,9 @@ post_update_to_api_extended() { fi local error_text="" error_text=$(get_error_text) - if [[ -n "$error_text" ]]; then - error=$(json_escape "$error_text") - else - error=$(json_escape "$(explain_exit_code "$exit_code")") - fi + local full_error + full_error=$(build_error_string "$exit_code" "$error_text") + error=$(json_escape "$full_error") error_category=$(categorize_error "$exit_code") [[ -z "$error" ]] && error="Unknown error" fi From 59ad12c7a0dc7ee506372715de0311dc7eb7665d Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:36:50 +0100 Subject: [PATCH 13/19] Install deps from requirements files Replace the previous `pip install calibreweb` and `uv sync` steps with explicit upgrades of pip/setuptools/wheel and installing dependencies from requirements.txt. If present, optional-requirements.txt is installed as well. Changes applied to both the installer and container update scripts to provide clearer, reproducible dependency management. --- ct/calibre-web.sh | 7 +++++-- install/calibre-web-install.sh | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/ct/calibre-web.sh b/ct/calibre-web.sh index d16139d35..7e07b20d5 100644 --- a/ct/calibre-web.sh +++ b/ct/calibre-web.sh @@ -44,8 +44,11 @@ function update_script() { msg_info "Installing Dependencies" cd /opt/calibre-web $STD uv venv - $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir calibreweb - $STD uv sync --no-dev --no-build-isolation + $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir --upgrade pip setuptools wheel + $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir -r requirements.txt + if [[ -f optional-requirements.txt ]]; then + $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir -r optional-requirements.txt + fi msg_ok "Installed Dependencies" msg_info "Restoring Data" diff --git a/install/calibre-web-install.sh b/install/calibre-web-install.sh index 41e971332..082e9f0ab 100644 --- a/install/calibre-web-install.sh +++ b/install/calibre-web-install.sh @@ -33,8 +33,11 @@ setup_uv msg_info "Installing Python Dependencies" cd /opt/calibre-web $STD uv venv -$STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir calibreweb -$STD uv sync --no-dev --no-build-isolation +$STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir --upgrade pip setuptools wheel +$STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir -r requirements.txt +if [[ -f optional-requirements.txt ]]; then + $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir -r optional-requirements.txt +fi msg_ok "Installed Python Dependencies" msg_info "Creating Service" From c5b92b60b929582dd535b7fe4730fee66c269860 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:37:26 +0100 Subject: [PATCH 14/19] Update discourse-install.sh --- install/discourse-install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/install/discourse-install.sh b/install/discourse-install.sh index 36385fa33..26fe4c8eb 100644 --- a/install/discourse-install.sh +++ b/install/discourse-install.sh @@ -24,6 +24,7 @@ $STD apt install -y \ git \ imagemagick \ gsfonts \ + brotli \ nginx \ redis-server msg_ok "Installed Dependencies" From b59702f31e053fefa386be49e918fdc2dddd471a Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:47:55 +0100 Subject: [PATCH 15/19] Update calibre-web-install.sh --- install/calibre-web-install.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install/calibre-web-install.sh b/install/calibre-web-install.sh index 082e9f0ab..63d671d60 100644 --- a/install/calibre-web-install.sh +++ b/install/calibre-web-install.sh @@ -15,7 +15,9 @@ update_os msg_info "Installing Dependencies" $STD apt install -y \ + build-essential \ python3 \ + python3-dev \ imagemagick \ libpango-1.0-0 \ libharfbuzz0b \ From f40baa4699f4004745c79cb814202a848d0e0a9d Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 11:54:14 +0100 Subject: [PATCH 16/19] admin creation --- ct/discourse.sh | 5 ++--- install/discourse-install.sh | 12 ++---------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/ct/discourse.sh b/ct/discourse.sh index e17884c8a..f17086fb3 100644 --- a/ct/discourse.sh +++ b/ct/discourse.sh @@ -72,6 +72,5 @@ msg_ok "Completed Successfully!\n" echo -e "${CREATING}${GN}${APP} setup has been successfully initialized!${CL}" echo -e "${INFO}${YW} Access it using the following URL:${CL}" echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}" -echo -e "${INFO}${YW} Default Credentials:${CL}" -echo -e "${TAB}${GATEWAY}${BGN}Username: admin${CL}" -echo -e "${TAB}${GATEWAY}${BGN}Password: Check /opt/discourse/.env${CL}" +echo -e "${INFO}${YW} Admin Setup:${CL}" +echo -e "${TAB}${GATEWAY}${BGN}Create the first account in the web UI (use admin@local to match developer emails)${CL}" diff --git a/install/discourse-install.sh b/install/discourse-install.sh index 26fe4c8eb..47a5cb0a4 100644 --- a/install/discourse-install.sh +++ b/install/discourse-install.sh @@ -105,16 +105,8 @@ set +a $STD bundle exec rails assets:precompile msg_ok "Built Discourse Assets" -msg_info "Creating Discourse Admin User" -cd /opt/discourse -export PATH="$HOME/.rbenv/bin:$HOME/.rbenv/shims:$PATH" -eval "$(rbenv init - bash)" 2>/dev/null || true -export RAILS_ENV=production -set -a -source /opt/discourse/.env -set +a -$STD bundle exec rails runner "User.create!(email: 'admin@local', username: 'admin', password: '${DISCOURSE_DB_PASS}', admin: true)" || true -msg_ok "Created Discourse Admin User" +msg_info "Preparing Admin Onboarding" +msg_ok "Automatic admin bootstrap skipped (use first signup in UI with admin@local)" msg_info "Creating Service" cat </etc/systemd/system/discourse.service From d83fc60493f8401045f0da4a754e5209829b4823 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 12:37:33 +0100 Subject: [PATCH 17/19] fix calbire web --- ct/calibre-web.sh | 13 ++++++------- install/calibre-web-install.sh | 5 +---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/ct/calibre-web.sh b/ct/calibre-web.sh index 7e07b20d5..00d96e3df 100644 --- a/ct/calibre-web.sh +++ b/ct/calibre-web.sh @@ -35,10 +35,11 @@ function update_script() { msg_ok "Stopped Service" msg_info "Backing up Data" - cp -r /opt/calibre-web/app.db /opt/calibre-web/app.db_backup 2>/dev/null + cp -r /opt/calibre-web/app.db /opt/app.db_backup + cp -r /opt/calibre-web/data /opt/data_backup msg_ok "Backed up Data" - CLEAN_INSTALL=1 fetch_and_deploy_gh_release "calibre-web" "janeczku/calibre-web" "tarball" "latest" "/opt/calibre-web" + CLEAN_INSTALL=1 fetch_and_deploy_gh_release "calibre-web" "janeczku/calibre-web" "prebuild" "latest" "/opt/calibre-web" "calibre-web*.tar.gz" setup_uv msg_info "Installing Dependencies" @@ -46,14 +47,12 @@ function update_script() { $STD uv venv $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir --upgrade pip setuptools wheel $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir -r requirements.txt - if [[ -f optional-requirements.txt ]]; then - $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir -r optional-requirements.txt - fi msg_ok "Installed Dependencies" msg_info "Restoring Data" - cp /opt/calibre-web/app.db_backup /opt/calibre-web/app.db 2>/dev/null - rm -f /opt/calibre-web/app.db_backup + cp /opt/app.db_backup /opt/calibre-web/app.db 2>/dev/null + cp -r /opt/data_backup /opt/calibre-web/data 2>/dev/null + rm -rf /opt/app.db_backup /opt/data_backup msg_ok "Restored Data" msg_info "Starting Service" diff --git a/install/calibre-web-install.sh b/install/calibre-web-install.sh index 63d671d60..7c7e85bdd 100644 --- a/install/calibre-web-install.sh +++ b/install/calibre-web-install.sh @@ -29,7 +29,7 @@ msg_info "Installing Calibre (for eBook conversion)" $STD apt install -y calibre msg_ok "Installed Calibre" -fetch_and_deploy_gh_release "calibre-web" "janeczku/calibre-web" "tarball" "latest" "/opt/calibre-web" +fetch_and_deploy_gh_release "calibre-web" "janeczku/calibre-web" "prebuild" "latest" "/opt/calibre-web" "calibre-web*.tar.gz" setup_uv msg_info "Installing Python Dependencies" @@ -37,9 +37,6 @@ cd /opt/calibre-web $STD uv venv $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir --upgrade pip setuptools wheel $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir -r requirements.txt -if [[ -f optional-requirements.txt ]]; then - $STD uv pip install --python /opt/calibre-web/.venv/bin/python --no-cache-dir -r optional-requirements.txt -fi msg_ok "Installed Python Dependencies" msg_info "Creating Service" From d773b939f7e145e11e9b5243f33ece8c13de2294 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:46:03 +0100 Subject: [PATCH 18/19] noHup /SIGHUP Tweak --- misc/api.func | 3 ++- misc/core.func | 13 +++++++++++++ misc/error_handler.func | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/misc/api.func b/misc/api.func index 21aa9b057..dad7a2ebe 100644 --- a/misc/api.func +++ b/misc/api.func @@ -178,6 +178,7 @@ explain_exit_code() { 126) echo "Command invoked cannot execute (permission problem?)" ;; 127) echo "Command not found" ;; 128) echo "Invalid argument to exit" ;; + 129) echo "Killed by SIGHUP (terminal closed / hangup)" ;; 130) echo "Aborted by user (SIGINT)" ;; 134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;; 137) echo "Killed (SIGKILL / Out of memory?)" ;; @@ -912,7 +913,7 @@ categorize_error() { 134 | 137) echo "resource" ;; # Signal/Process errors (SIGTERM, SIGPIPE, SIGSEGV) - 139 | 141 | 143) echo "signal" ;; + 129 | 139 | 141 | 143) echo "signal" ;; # Shell errors (general error, syntax error) 1 | 2) echo "shell" ;; diff --git a/misc/core.func b/misc/core.func index e4c7efcc6..74b98ac4f 100644 --- a/misc/core.func +++ b/misc/core.func @@ -1641,4 +1641,17 @@ function get_lxc_ip() { # SIGNAL TRAPS # ============================================================================== +# ------------------------------------------------------------------------------ +# on_hup_keepalive() +# +# - SIGHUP (terminal hangup) trap handler +# - Keeps long-running scripts alive if terminal/SSH session disconnects +# - Stops spinner safely and writes warning to active log +# ------------------------------------------------------------------------------ +on_hup_keepalive() { + stop_spinner + log_msg "[WARN] Received SIGHUP (terminal hangup). Continuing execution in background." +} + +trap 'on_hup_keepalive' HUP trap 'stop_spinner' EXIT INT TERM diff --git a/misc/error_handler.func b/misc/error_handler.func index 87c2b4883..bd6d98929 100644 --- a/misc/error_handler.func +++ b/misc/error_handler.func @@ -49,6 +49,7 @@ if ! declare -f explain_exit_code &>/dev/null; then 126) echo "Command invoked cannot execute (permission problem?)" ;; 127) echo "Command not found" ;; 128) echo "Invalid argument to exit" ;; + 129) echo "Killed by SIGHUP (terminal closed / hangup)" ;; 130) echo "Terminated by Ctrl+C (SIGINT)" ;; 134) echo "Process aborted (SIGABRT - possibly Node.js heap overflow)" ;; 137) echo "Killed (SIGKILL / Out of memory?)" ;; From 56aaae7e6613ee10f0891a858ef5e7b866891284 Mon Sep 17 00:00:00 2001 From: "CanbiZ (MickLesk)" <47820557+MickLesk@users.noreply.github.com> Date: Tue, 17 Feb 2026 15:22:42 +0100 Subject: [PATCH 19/19] Update calibre-web-install.sh --- install/calibre-web-install.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install/calibre-web-install.sh b/install/calibre-web-install.sh index 7c7e85bdd..786a2b77c 100644 --- a/install/calibre-web-install.sh +++ b/install/calibre-web-install.sh @@ -18,6 +18,9 @@ $STD apt install -y \ build-essential \ python3 \ python3-dev \ + libldap2-dev \ + libsasl2-dev \ + libssl-dev \ imagemagick \ libpango-1.0-0 \ libharfbuzz0b \