diff --git a/.gitignore b/.gitignore index 8786ca8..df6ee3f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,19 @@ Configurations/*.bak # ── Runtime state, data, logs ───────────────────────────────────────────────── data/ +State_Files/ *.log *.lock +# ── Plugin config files (co-located with scripts on flash) ─────────────────── +varaverk.cfg +varaverk.cron +varaverk-*.txz + +# ── Build artifacts ─────────────────────────────────────────────────────────── +# .txz packages are attached to GitHub releases, not committed to the repo. +Plugin/dist/ + # ── OS / editor ─────────────────────────────────────────────────────────────── .DS_Store *.swp diff --git a/Deployment/conf_templates/master.conf b/Deployment/conf_templates/master.conf index cc956a6..3669443 100644 --- a/Deployment/conf_templates/master.conf +++ b/Deployment/conf_templates/master.conf @@ -111,11 +111,14 @@ # ============================================================================================== # ── SHARED HOST CONFIGURATION ───────────────────────────────────────────────────────────────── # ============================================================================================== -# DATA_DIR is the same path on all servers — persistent script state and statistics. -# Array share — survives reboots, no flash drive wear. -# Created automatically if it doesn't exist. -# Only truly critical files (fallback state, watchdog reboot log) stay on /boot/config. - DATA_DIR="/mnt/user/appdata/Varaverk/data" +# All Varaverk persistent data lives under /mnt/user/appdata/Varaverk/. +# Both directories are created automatically if they don't exist. +# +# DATA_DIR — historical logs, statistics, discovery histories, blocklists +# STATE_DIR — runtime state files for all scripts (watchdogs, fallback, transcode, etc.) +# Requirement: ALL state files MUST use $STATE_DIR. No /tmp, no /boot/config. + DATA_DIR="/boot/config/plugins/varaverk/data" + STATE_DIR="/boot/config/plugins/varaverk/State_Files" # ── Version Parity ── # Controls behaviour when local and remote unRAID versions differ. @@ -152,9 +155,11 @@ # All identity vars (hostnames, SSH keys) live in host*.conf. # Hostnames already match Tailscale device names — IP resolution is automatic. # -# State files on /boot/config — survives reboots, available before array starts: -# /boot/config/partnership_HOST1.db ← HOST1 writes only -# /boot/config/partnership_HOST2.db ← HOST2 writes only +# State files in STATE_DIR — survives reboots, array must be up (scripts run after array start): +# STATE_DIR/partnership_HOST1.db ← HOST1 writes only +# STATE_DIR/partnership_HOST2.db ← HOST2 writes only +# STATE_DIR/partnership_blocklist.db ← shared blocklist +# STATE_DIR/varaverk_setup.db ← onboarding progress state # Propagated via SSH — no rsync needed # # critical_sync_maintenance.sh runs --check every 30min: @@ -185,6 +190,11 @@ PARTNERSHIP_OFFLINE_THRESHOLD=30 # days either server unreachable before auto-offboard # works both directions independently +# Partnership and setup state files — all in STATE_DIR per the project requirement. +# Scripts use these variables; do not hardcode /boot/config paths in scripts. + PARTNERSHIP_BLOCKLIST_FILE="$STATE_DIR/partnership_blocklist.db" + VARAVERK_SETUP_FILE="$STATE_DIR/varaverk_setup.db" + # Tailscale removal on offboard. PARTNERSHIP_REMOVE_TAILSCALE=true # remove mirror from Tailscale tailnet on offboard # false = skip removal (manual or testing) @@ -250,7 +260,7 @@ GITEA_CONTAINER="Gitea" GITEA_REPO_PATH="FailedProxy/Varaverk.git" GITEA_DOMAIN="" # e.g. git.yourdomain.com — requires NPM + DNS - TARGET_DIR="/mnt/user/appdata/Varaverk" + TARGET_DIR="/boot/config/plugins/varaverk" GITEA_SSH_KEY="/root/.ssh/unraid_gitea" SSH_PORT=221 # Gitea SSH port (default 22, Gitea often uses 221/222) GITEA_HTTP_PORT=3000 # Gitea web/API port — used by gitea_ssh_setup.sh @@ -420,7 +430,7 @@ MONTHLY_UPTIME_THRESHOLD_DAYS=30 # minimum uptime in days before maintenance fires MONTHLY_RUN_INTERVAL_DAYS=30 # minimum days since last run before running again - MONTHLY_LAST_RUN_FILE="/boot/config/monthly_maintenance_last_run.db" + MONTHLY_LAST_RUN_FILE="$STATE_DIR/monthly_maintenance_last_run.db" # ━━━ Sunday Morning Coffee Report ━━━ # Orchestrator that runs all Sunday monitor scripts in sequence. @@ -633,7 +643,7 @@ EXTERNAL_IP="8.8.8.8" FALLBACK_CHECK_INTERVAL=30 # seconds between fallback state checks FALLBACK_HANDBACK_STRIKES=3 # consecutive healthy checks before initiating handback (3×30s = 90s) - FALLBACK_STATE_FILE="/boot/config/fallback_state.db" + FALLBACK_STATE_FILE="$STATE_DIR/fallback_state.db" FALLBACK_ENABLED=true # HOST2 back online # false = suppresses "not running" warnings in status scripts FALLBACK_PARTNERSHIP_REQUIRED=true # gate fallback on an active partnership @@ -700,8 +710,8 @@ # HOST*_WATCHDOG_SCAN_IGNORE — skip in Tier 2 scan # HOST*_WATCHDOG_DEPENDENCIES — dependency ordering for restart decisions -# Strike state file — /tmp resets on reboot (correct — no stale strikes after reboot) - WATCHDOG_STATE_FILE="/tmp/container_watchdog_state.db" +# Strike state file — persistent in STATE_DIR + WATCHDOG_STATE_FILE="$STATE_DIR/container_watchdog_state.db" # CPU thresholds — normalised against total core count at runtime SOFT_CPU_THRESHOLD=80 # warn at this % of total system CPU @@ -768,8 +778,8 @@ WATCHDOG_APPDATA_LOG_MAX_GB=2 # flag *.log files exceeding this size (absolute) WATCHDOG_APPDATA_TRUNCATE_LOGS=false # set true to auto-truncate oversized *.log files on action cycle WATCHDOG_APPDATA_STRIKE_LIMIT=3 # cycles before action fires (matches existing watchdog pattern) - WATCHDOG_APPDATA_GROWTH_FILE="/tmp/watchdog_appdata_growth.db" # /tmp — resets on reboot ✅ - STORAGE_WATCHDOG_STATE_FILE="/tmp/storage_watchdog_state.db" # /tmp — resets on reboot ✅ + WATCHDOG_APPDATA_GROWTH_FILE="$STATE_DIR/watchdog_appdata_growth.db" + STORAGE_WATCHDOG_STATE_FILE="$STATE_DIR/storage_watchdog_state.db" # ━━━ Docker Network Connect ━━━ # Ensures custom networks exist and connects containers at array start. @@ -855,7 +865,7 @@ NETWORK_WATCHDOG_CHECK_TAILSCALE=true NETWORK_WATCHDOG_NPM_TIMEOUT=10 NETWORK_WATCHDOG_NPM_STRIKE_LIMIT=2 - NETWORK_WATCHDOG_NPM_STATE_FILE="/tmp/network_watchdog_state.db" + NETWORK_WATCHDOG_NPM_STATE_FILE="$STATE_DIR/network_watchdog_state.db" # ━━━ WebGUI Watchdog ━━━ # Monitors unRAID WebGUI responsiveness — escalates through nginx restart → emhttp restart. @@ -1164,6 +1174,7 @@ TRANSCODE_MANAGER_MODE="smart" # Daily statistics log — read by weekly_health_digest.sh for transcode summary. + TRANSCODE_STATE_FILE="$STATE_DIR/transcode_state.db" TRANSCODE_DAILY_LOG="$DATA_DIR/transcode_daily.db" TRANSCODE_LOG_RETENTION=90 # days before old entries purged @@ -1209,7 +1220,7 @@ # ━━━ ZFS Memory Snapshot ━━━ # Weekly ZFS pool health and memory diagnostic report — informational only. - ZFS_REPORT_LOG="/var/log/zfs-weekly-health.log" + ZFS_REPORT_LOG="$DATA_DIR/zfs-weekly-health.log" ZFS_REPORT_ARC_WARN_PCT=90 # warn if ARC using more than this % of its max ZFS_REPORT_FREE_WARN_GB=10 # warn if less than this GB free RAM ZFS_REPORT_AVAIL_WARN_GB=20 # warn if less than this GB available on ZFS pool @@ -1264,7 +1275,7 @@ # HOST*_RW_STOP_CONTAINERS — docker stop at hard pressure (in host*.conf) RW_ENABLED=true - RW_STATE_FILE="/tmp/resource_watchdog_state.db" + RW_STATE_FILE="$STATE_DIR/resource_watchdog_state.db" # ━━━ Pressure Thresholds ━━━ # Graduated RAM response — resource_watchdog acts before system_watchdog reboots. @@ -1334,10 +1345,10 @@ # Warn/shutdown/recover RAM tiers are handled by resource_watchdog.sh # ━━━ State Files ━━━ - SYS_WATCHDOG_STATE_FILE="/tmp/system_watchdog_state.db" # /tmp — resets on reboot ✅ - SYS_WATCHDOG_FAILED_FILE="/boot/config/system_watchdog_failed.db" - SYS_WATCHDOG_REBOOT_LOG="/boot/config/system_watchdog_reboots.db" - SYS_WATCHDOG_OOM_FILE="/tmp/system_watchdog_oom.db" # /tmp — resets on reboot ✅ + SYS_WATCHDOG_STATE_FILE="$STATE_DIR/system_watchdog_state.db" + SYS_WATCHDOG_FAILED_FILE="$STATE_DIR/system_watchdog_failed.db" + SYS_WATCHDOG_REBOOT_LOG="$STATE_DIR/system_watchdog_reboots.db" + SYS_WATCHDOG_OOM_FILE="$STATE_DIR/system_watchdog_oom.db" # ━━━ Strike and Reboot Loop Settings ━━━ # Strike system: a check must fail this many consecutive cycles before action is taken. diff --git a/Deployment/deploy.sh b/Deployment/deploy.sh index dce2a99..f6aa515 100755 --- a/Deployment/deploy.sh +++ b/Deployment/deploy.sh @@ -14,7 +14,7 @@ # ============================================================================================== DEV_ROOT="/mnt/cloud-storage/Important Shit/Git/Development/Varaverk" -PROD_ROOT="/mnt/user/appdata/Varaverk" +PROD_ROOT="/boot/config/plugins/varaverk" UPGRADE="$PROD_ROOT/Deployment/conf_upgrade.sh" LOG="/tmp/vv_deploy.log" diff --git a/Fallback/fallback.sh b/Fallback/fallback.sh index 095f5fc..913d41d 100755 --- a/Fallback/fallback.sh +++ b/Fallback/fallback.sh @@ -345,7 +345,7 @@ state_init() { # Returns 0 (true) if the local partnership DB reports an ACTIVE partnership. # Reads /boot/config/partnership_.db — no subprocess, no SSH. check_partnership_active() { - local state_file="/boot/config/partnership_${LOCAL_SERVER_NAME}.db" + local state_file="${STATE_DIR:-/boot/config}/partnership_${LOCAL_SERVER_NAME}.db" local state state=$(grep "^state=" "$state_file" 2>/dev/null | cut -d= -f2) [[ "$state" == "ACTIVE" ]] diff --git a/Orchestrators/monthly_maintenance.sh b/Orchestrators/monthly_maintenance.sh index d4d5d82..ddf6328 100644 --- a/Orchestrators/monthly_maintenance.sh +++ b/Orchestrators/monthly_maintenance.sh @@ -72,7 +72,7 @@ detect_hosts # Defaults — overridden by master.conf values MONTHLY_UPTIME_THRESHOLD_DAYS="${MONTHLY_UPTIME_THRESHOLD_DAYS:-30}" MONTHLY_RUN_INTERVAL_DAYS="${MONTHLY_RUN_INTERVAL_DAYS:-30}" -MONTHLY_LAST_RUN_FILE="${MONTHLY_LAST_RUN_FILE:-/boot/config/monthly_maintenance_last_run.db}" +MONTHLY_LAST_RUN_FILE="${MONTHLY_LAST_RUN_FILE:-${STATE_DIR:-/tmp}/monthly_maintenance_last_run.db}" UPTIME_THRESHOLD_SECS=$(( MONTHLY_UPTIME_THRESHOLD_DAYS * 86400 )) INTERVAL_SECS=$(( MONTHLY_RUN_INTERVAL_DAYS * 86400 )) diff --git a/Orchestrators/watchdog_orchestrator.sh b/Orchestrators/watchdog_orchestrator.sh index 30de498..56d6e4c 100755 --- a/Orchestrators/watchdog_orchestrator.sh +++ b/Orchestrators/watchdog_orchestrator.sh @@ -164,7 +164,7 @@ DURATION=$(( CYCLE_END - CYCLE_START )) # ============================================================================================== if [[ "${WATCHDOG_ORCHESTRATOR_HEARTBEAT:-true}" == true ]]; then HB_SECONDS=$(( ${WATCHDOG_ORCHESTRATOR_HEARTBEAT_HOURS:-1} * 3600 )) - HB_COUNT_FILE="/tmp/watchdog_orch_hb.count" + HB_COUNT_FILE="${STATE_DIR:-/tmp}/watchdog_orch_hb.count" HB_COUNT=$(cat "$HB_COUNT_FILE" 2>/dev/null || echo 0) HB_COUNT=$(( HB_COUNT + 1 )) echo "$HB_COUNT" > "$HB_COUNT_FILE" diff --git a/Partnership/onboard_cancel.sh b/Partnership/onboard_cancel.sh index eecf7e4..42d49d8 100755 --- a/Partnership/onboard_cancel.sh +++ b/Partnership/onboard_cancel.sh @@ -54,7 +54,7 @@ OWNER_ID="${PARTNERSHIP_OWNER_HOST:-HOST1}" MIRROR_ID=$( [[ "$OWNER_ID" == "HOST1" ]] && echo "HOST2" || echo "HOST1" ) MIRROR="${!MIRROR_ID}" SSH_KEY_PUB="${SSH_KEY}.pub" -STATE_FILE="/boot/config/varaverk_setup.db" +STATE_FILE="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}" AUTH_KEYS="/root/.ssh/authorized_keys" MIRROR_SHORT="${MIRROR%%.*}" diff --git a/Partnership/partnership_manager.sh b/Partnership/partnership_manager.sh index 6667235..c9d650d 100644 --- a/Partnership/partnership_manager.sh +++ b/Partnership/partnership_manager.sh @@ -189,7 +189,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" SSH_TIMEOUT=15 -BLOCKLIST_FILE="/boot/config/partnership_blocklist.db" +BLOCKLIST_FILE="${PARTNERSHIP_BLOCKLIST_FILE:-${STATE_DIR:-/boot/config}/partnership_blocklist.db}" # ── Parse mode flags before parse_args ──────────────────────────────────────────────────────── MODE="" @@ -267,11 +267,11 @@ AM_MIRROR=false [[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true # State files -LOCAL_STATE_FILE="/boot/config/partnership_${LOCAL_SERVER_NAME}.db" -REMOTE_STATE_FILE="/boot/config/partnership_${REMOTE_SERVER_NAME}.db" -OWNER_STATE_FILE="/boot/config/partnership_${OWNER}.db" -MIRROR_STATE_FILE="/boot/config/partnership_${MIRROR}.db" -OFFLINE_COUNTER="/boot/config/partnership_offline_days.db" +LOCAL_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${LOCAL_SERVER_NAME}.db" +REMOTE_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${REMOTE_SERVER_NAME}.db" +OWNER_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${OWNER}.db" +MIRROR_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${MIRROR}.db" +OFFLINE_COUNTER="${STATE_DIR:-/boot/config}/partnership_offline_days.db" # ── Exit Trap — restart locally stopped containers if script crashes mid-cleanup ────────────── # Used by folderview3_remove_partner_folder() and cleanup_partner_containers() — also shared @@ -1463,7 +1463,7 @@ if (vv_write_conf_raw('master.conf', \$master)) { fi # Write HOST1_LOCAL_DONE flag to setup.db - local_state_file="/boot/config/varaverk_setup.db" + local_state_file="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}" if [[ "$DRY_RUN" == false ]]; then flag_key="${MY_ID}_LOCAL_DONE" if grep -q "^${flag_key}=" "$local_state_file" 2>/dev/null; then diff --git a/Partnership/partnership_offboard.sh b/Partnership/partnership_offboard.sh index e6d832a..6cef246 100755 --- a/Partnership/partnership_offboard.sh +++ b/Partnership/partnership_offboard.sh @@ -117,11 +117,11 @@ AM_MIRROR=false [[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true [[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true -LOCAL_STATE_FILE="/boot/config/partnership_${LOCAL_SERVER_NAME}.db" -REMOTE_STATE_FILE="/boot/config/partnership_${REMOTE_SERVER_NAME}.db" -OWNER_STATE_FILE="/boot/config/partnership_${OWNER}.db" -MIRROR_STATE_FILE="/boot/config/partnership_${MIRROR}.db" -OFFLINE_COUNTER="/boot/config/partnership_offline_days.db" +LOCAL_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${LOCAL_SERVER_NAME}.db" +REMOTE_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${REMOTE_SERVER_NAME}.db" +OWNER_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${OWNER}.db" +MIRROR_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${MIRROR}.db" +OFFLINE_COUNTER="${STATE_DIR:-/boot/config}/partnership_offline_days.db" acquire_lock "strict" diff --git a/Partnership/partnership_onboard.sh b/Partnership/partnership_onboard.sh index cd9a563..05b67c8 100755 --- a/Partnership/partnership_onboard.sh +++ b/Partnership/partnership_onboard.sh @@ -204,7 +204,7 @@ START=$(date +%s) write_onboard_phase() { local target_id="$1" phase="$2" local key="${target_id}_PHASE${phase}_DONE" - local state_file="/boot/config/varaverk_setup.db" + local state_file="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}" [[ "$DRY_RUN" == true ]] && { warn "DRY RUN — would write ${key}=true"; return 0; } if grep -q "^${key}=" "$state_file" 2>/dev/null; then sed -i "s|^${key}=.*|${key}=true|" "$state_file" @@ -497,7 +497,7 @@ if [[ "$AM_MIRROR" == true ]]; then OWNER_SCRIPTS_DIR=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \ -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \ 'grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null | cut -d= -f2 | tr -d "\"'"'"'" 2>/dev/null' 2>/dev/null | tr -d '[:space:]') - OWNER_SCRIPTS_DIR="${OWNER_SCRIPTS_DIR:-/mnt/user/appdata/Varaverk}" + OWNER_SCRIPTS_DIR="${OWNER_SCRIPTS_DIR:-/boot/config/plugins/varaverk}" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would SSH to $OWNER ($OWNER_IP) and trigger Phase 2" @@ -592,9 +592,10 @@ elif [[ "$PHASE1_ONLY" == true ]]; then # Write key-ready flag so UI can show the manual-install state [[ "$DRY_RUN" == false ]] && { local kflag="${MIRROR_ID}_KEY_READY" - grep -q "^${kflag}=" /boot/config/varaverk_setup.db 2>/dev/null \ - && sed -i "s|^${kflag}=.*|${kflag}=true|" /boot/config/varaverk_setup.db \ - || echo "${kflag}=true" >> /boot/config/varaverk_setup.db + local _setup_f="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}" + grep -q "^${kflag}=" "$_setup_f" 2>/dev/null \ + && sed -i "s|^${kflag}=.*|${kflag}=true|" "$_setup_f" \ + || echo "${kflag}=true" >> "$_setup_f" } fi STEP_SSH_OK=false diff --git a/Partnership/partnership_transfer.sh b/Partnership/partnership_transfer.sh index bc6727d..d8e6993 100755 --- a/Partnership/partnership_transfer.sh +++ b/Partnership/partnership_transfer.sh @@ -145,8 +145,8 @@ AM_MIRROR=false [[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true [[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true -LOCAL_STATE_FILE="/boot/config/partnership_${LOCAL_SERVER_NAME}.db" -REMOTE_STATE_FILE="/boot/config/partnership_${REMOTE_SERVER_NAME}.db" +LOCAL_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${LOCAL_SERVER_NAME}.db" +REMOTE_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${REMOTE_SERVER_NAME}.db" acquire_lock "strict" @@ -295,7 +295,7 @@ if [[ "$DRY_RUN" == false ]]; then -o ConnectTimeout="$SSH_TIMEOUT" \ -o StrictHostKeyChecking=no \ "$SCRIPTS_ROOT/Configurations/master.conf" \ - "root@${MIRROR_IP}:/mnt/user/appdata/Varaverk/Configurations/master.conf" 2>/dev/null && \ + "root@${MIRROR_IP}:/boot/config/plugins/varaverk/Configurations/master.conf" 2>/dev/null && \ log "master.conf pushed to $NEW_OWNER ✅" || \ error "Failed to push master.conf to $NEW_OWNER — set PARTNERSHIP_OWNER_HOST=\"$NEW_OWNER_ID\" manually" else diff --git a/Plugin/Icons/Varaverk.png b/Plugin/Icons/Varaverk.png new file mode 100755 index 0000000..8539461 Binary files /dev/null and b/Plugin/Icons/Varaverk.png differ diff --git a/Plugin/Manual-Plugin.md b/Plugin/Manual-Plugin.md new file mode 100644 index 0000000..994f57f --- /dev/null +++ b/Plugin/Manual-Plugin.md @@ -0,0 +1,131 @@ +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# 🔌 PLUGIN — Manual +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Setup procedures, deployment steps, and operational reference. +For folder overview see `README-Plugin.md`. For web app logic see the headers in `unraid/include/`. + +--- + +## ━━━ FIRST-TIME INSTALL ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +### Prerequisites + +- Repo cloned onto the target Unraid server +- `/boot/config/plugins/varaverk/varaverk.plg` exists on flash (see PLG SETUP below) + +### Steps + +```bash +cd Plugin/ +./dev_install.sh +``` + +This creates: + +``` +/usr/local/emhttp/plugins/varaverk → Plugin/unraid/ (symlink) +``` + +Changes to any file under `Plugin/unraid/` take effect immediately in the browser — +no restart, no reinstall. + +### Verify + +Navigate to the Unraid web UI. **Varaverk** should appear in the Tasks menu. +Go to **Settings → Other Settings** — a Varaverk tile should also appear there. + +--- + +## ━━━ PLG SETUP ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +The `.plg` file registers Varaverk with Unraid's plugin system. It enables the cron +mechanism (`update_cron`) and makes the plugin appear on the **Plugins** management page. + +Create it once on flash — it persists across reboots: + +```bash +mkdir -p /boot/config/plugins/varaverk + +cat > /boot/config/plugins/varaverk.plg <<'EOF' + + + + +]> + + +EOF +``` + +The `.plg` has no packages and no remote URLs — it is local-only and is not published +to Community Applications. + +**Cron flow on every boot:** +1. `event/disks_mounted/rebuild_cron` fires +2. Copies `varaverk.plg` to `/var/log/plugins/` +3. Calls `vv_cron_rebuild()` → writes `varaverk.cron` → calls `update_cron` +4. Unraid merges `varaverk.cron` into `/etc/cron.d/root` +5. crond picks up all Varaverk jobs + +--- + +## ━━━ SCRIPTS DIRECTORY SETTING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +`SCRIPTS_DIR` is the only plugin-level setting. It tells the plugin where to find the +Configurations directory and all scripts. + +**Set it via:** Settings → Other Settings → Varaverk → Scripts directory + +Default: `/mnt/user/appdata/Varaverk` + +The value is stored in `/boot/config/plugins/varaverk/varaverk.cfg`: + +```bash +SCRIPTS_DIR="/mnt/user/appdata/Varaverk" +``` + +All other configuration lives in `Configurations/master.conf` and `Configurations/host*.conf`. + +--- + +## ━━━ REPO MOVE PROCEDURE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +If the repo is cloned to a new path: + +1. Re-run `dev_install.sh` — removes the stale symlink and creates a new one pointing at the new path +2. Update `SCRIPTS_DIR` in Settings → Other Settings → Varaverk (or edit `varaverk.cfg` directly on flash) + +The `.plg` on flash does not need to change — it has no path references. + +--- + +## ━━━ UPDATING THE PLUGIN VERSION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +The version in `/boot/config/plugins/varaverk.plg` is cosmetic for a local plugin — +Unraid does not check it against anything remote. Update it when you want the Plugins +management page to reflect when the plugin was last changed: + +```xml + +``` + +--- + +## ━━━ ADDING A NEW OS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +`dev_install.sh` is built to support multiple OS targets. To add one: + +1. Create `Plugin//` with the OS-appropriate web app files +2. Add the OS marker to `detect_os()` in `dev_install.sh`: + ```bash + elif [[ -f /etc/-marker ]]; then echo "" + ``` +3. Add the install target to the `case` block: + ```bash + ) + SOURCE="$SCRIPT_DIR/" + TARGET="/path/to/web/server/plugins/$PLUGIN_NAME" + ;; + ``` diff --git a/Plugin/README-Plugin.md b/Plugin/README-Plugin.md new file mode 100644 index 0000000..a126329 --- /dev/null +++ b/Plugin/README-Plugin.md @@ -0,0 +1,86 @@ +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# 🔌 PLUGIN +# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +**The Varaverk Unraid plugin — a web UI that wraps the entire script ecosystem.** +Scheduler, Monitor, Docker management, Partnership sync, Fallback state, and Arrs — +all surfaced inside the Unraid web interface as a first-class plugin. + +> **Why this folder exists:** The scripts need a control surface. Managing a 50+ container +> homelab ecosystem from terminal windows is friction. The plugin turns configuration files +> into editable forms, cron schedules into a visual scheduler, and runtime log output into +> a live dashboard — without duplicating any of the logic that already lives in common.sh +> and the conf files. + +--- + +## ━━━ THE PROBLEM THAT BUILT THIS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +The script ecosystem works well from the command line, but day-to-day operation is not +the command line. Checking whether the nightly sync ran, adjusting a container's watchdog +limit, confirming the partnership fallback is active — all of that requires SSH sessions, +knowing which log files to look at, and remembering which conf variable controls what. + +The plugin solves the visibility problem: one URL on any browser, on any device on the +Tailscale network, shows everything running and lets you act on it. No extra tooling, +no separate monitoring stack, no third-party dashboards. + +--- + +## ━━━ WHAT THIS FOLDER CONTAINS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +``` +Plugin/ +├── dev_install.sh # One-time developer setup: symlinks plugin into web server +├── Icons/ # Source icon assets (1024px master files) +└── unraid/ # The Unraid plugin application + ├── Varaverk.page # Main plugin entry point (Tasks menu) + ├── VaraverkSettings.page # Unraid Settings → Other Settings entry + ├── api/ # PHP API endpoints (called by JS via fetch) + ├── css/ # Plugin stylesheet + ├── event/ # Unraid event hooks (boot-time cron setup, array lifecycle) + ├── icons/ # Plugin icons served by emhttp + ├── images/ # Plugin images + ├── include/ # PHP business logic shared across pages + ├── js/ # Frontend JavaScript + ├── pages/ # Per-tab page includes (monitor, scheduler, docker, ...) + └── run_job.sh # Script runner invoked by the Scheduler +``` + +--- + +## ━━━ RELATIONSHIP TO THE REST OF THE REPO ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +**Plugin is a wrapper, never a reimplementation.** Every setting the plugin reads or writes +lives in `Configurations/master.conf` or `Configurations/host*.conf` — the same files the +shell scripts read. The plugin has no separate data store. If a conf file changes outside +the plugin (by hand, by SSH), the plugin reflects it on next load. + +The one exception is `varaverk.cfg` on flash (`/boot/config/plugins/varaverk/varaverk.cfg`), +which holds a single bootstrap value: `SCRIPTS_DIR`. This is the path the plugin uses to +find the Configurations directory and all scripts. Everything else flows from there. + +The plugin also taps `common.sh` indirectly — `include/config.php` mirrors +`resolve_tailscale_ip()` and `detect_host()` exactly, using the same logic as common.sh +so behaviour stays consistent without a shell dependency. + +--- + +## ━━━ SCRIPTS IN THIS FOLDER ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +| Script | Role | When It Runs | +|--------|------|--------------| +| `dev_install.sh` | Symlinks `Plugin/unraid/` into Unraid's web server | Once, manually, after cloning or moving the repo | + +--- + +## ━━━ UNRAID INTEGRATION POINTS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +| File | Where it appears in Unraid | +|------|---------------------------| +| `Varaverk.page` | Tasks menu item | +| `VaraverkSettings.page` | Settings → Other Settings tile | +| `event/disks_mounted/rebuild_cron` | Fires on every boot — copies `.plg`, rebuilds cron | +| `event/disks_mounted/array_start_jobs` | Fires when array starts | +| `event/disks_unmounting/array_stop_jobs` | Fires when array stops | +| `/boot/config/plugins/varaverk.plg` | Registers the plugin with Unraid's plugin system (lives on flash, not in repo) | diff --git a/Plugin/build.sh b/Plugin/build.sh new file mode 100755 index 0000000..da57b3d --- /dev/null +++ b/Plugin/build.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# ============================================================================================== +# ============================= build.sh ======================================================= +# ============================================================================================== +# +# PURPOSE +# ───────────────────────────────────────────────────────────────────────────── +# Packages the plugin web files (Plugin/unraid/) into a Slackware .txz, the +# format unRAID re-installs from flash on every boot. This is the RELEASE path — +# for day-to-day development use dev_install.sh (symlink, instant edits). +# +# What it produces (in Plugin/dist/): +# varaverk--noarch-1.txz the package unRAID installs to +# /usr/local/emhttp/plugins/varaverk/ +# varaverk--noarch-1.txz.sha256 +# +# It also rewrites the and lines in +# Plugin/varaverk.plg so the .plg always points at the package just built. +# +# WHY a .txz (not the dev symlink): +# rc.local runs `plugin install` on every .plg at boot, BEFORE the array +# mounts. A symlink into /mnt/user/appdata can't be made that early (appdata +# isn't mounted) and the disks_mounted event hooks live behind that missing +# symlink — chicken-and-egg. A .txz lives on flash, so unRAID extracts it to +# RAM before the array starts; the event hooks are then present in time to +# rebuild cron when the array mounts. Scripts stay on appdata (git clone). +# +# ============================================================================================== +# USAGE +# ============================================================================================== +# ./build.sh # version = today's date (YYYY.MM.DD) +# ./build.sh 2026.09.01 # explicit version +# +# After building: commit Plugin/dist/ + the updated .plg, then attach the +# .txz to a GitHub release tagged so the .plg URL resolves for +# downloaders. (The .plg also works offline if the .txz is already cached on +# flash with a matching SHA256.) +# ============================================================================================== + +set -euo pipefail + +PLUGIN_NAME="varaverk" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SRC="$SCRIPT_DIR/unraid" +DIST="$SCRIPT_DIR/dist" +PLG="$SCRIPT_DIR/varaverk.plg" + +VERSION="${1:-$(date +%Y.%m.%d)}" +PKG_BASENAME="${PLUGIN_NAME}-${VERSION}-noarch-1" +OUTFILE="$DIST/${PKG_BASENAME}.txz" + +# ── Guards ────────────────────────────────────────────────────────────────── +[[ -d "$SRC" ]] || { echo "ERROR: source not found: $SRC"; exit 1; } +command -v makepkg >/dev/null || { echo "ERROR: makepkg not found (run on unRAID)"; exit 1; } + +mkdir -p "$DIST" + +# ── Stage files under the real install path ───────────────────────────────── +# installpkg extracts relative to / so the package must contain the full path. +STAGE="$(mktemp -d)" +trap 'rm -rf "$STAGE"' EXIT +INSTALL_ROOT="$STAGE/usr/local/emhttp/plugins/$PLUGIN_NAME" +mkdir -p "$INSTALL_ROOT" + +# Copy web files; drop VCS/editor cruft and any dev-only leftovers. +cp -a "$SRC/." "$INSTALL_ROOT/" +find "$INSTALL_ROOT" -name '.git*' -prune -exec rm -rf {} + 2>/dev/null || true +find "$INSTALL_ROOT" -name '*.swp' -delete 2>/dev/null || true + +# Normalise ownership/permissions inside the package. +chown -R root:root "$STAGE" 2>/dev/null || true +find "$INSTALL_ROOT" -type d -exec chmod 0755 {} + +find "$INSTALL_ROOT" -type f -exec chmod 0644 {} + +# Keep shell/event scripts executable. +find "$INSTALL_ROOT" \( -name '*.sh' -o -path '*/event/*' \) -type f -exec chmod 0755 {} + + +# ── Build the package ─────────────────────────────────────────────────────── +# makepkg doesn't quote its output arg internally, so build to a space-free temp +# path (the repo lives under a dir with spaces) then move into dist/. +TMP_OUT="$STAGE.txz" +( cd "$STAGE" && makepkg -l y -c y "$TMP_OUT" >/dev/null ) +mv "$TMP_OUT" "$OUTFILE" + +# ── Hash + record ─────────────────────────────────────────────────────────── +SHA256="$(sha256sum "$OUTFILE" | awk '{print $1}')" +echo "$SHA256 ${PKG_BASENAME}.txz" > "$OUTFILE.sha256" + +# ── Point the .plg at this build ──────────────────────────────────────────── +if [[ -f "$PLG" ]]; then + sed -i -E "s|()|\1${VERSION}\2|" "$PLG" + sed -i -E "s|()|\1${SHA256}\2|" "$PLG" +fi + +SIZE="$(numfmt --to=iec "$(stat -c %s "$OUTFILE")")" +cat < $hostId, + 'key_present' => $key !== '', + 'key_prefix' => $key ? substr($key, 0, 8) . '...' : null, + 'curl_available' => function_exists('curl_init'), + 'allow_url_fopen'=> (bool)ini_get('allow_url_fopen'), + 'debug_log' => null, + 'probe' => null, + 'probe_raw' => null, +]; + +// Show last debug log if present +$debugFile = '/tmp/vv_api_debug.json'; +if (file_exists($debugFile)) { + $result['debug_log'] = json_decode(file_get_contents($debugFile), true); +} + +// Run a minimal probe query +if ($key) { + $url = 'http://localhost/graphql'; + $body = json_encode(['query' => '{ info { os { hostname } } }']); + + if (function_exists('curl_init')) { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/json', "x-api-key: {$key}"], + CURLOPT_POSTFIELDS => $body, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 5, + CURLOPT_CONNECTTIMEOUT => 3, + ]); + $raw = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlErr = curl_error($ch); + curl_close($ch); + } else { + $ctx = stream_context_create(['http' => [ + 'method' => 'POST', + 'header' => "Content-Type: application/json\r\nx-api-key: {$key}", + 'content' => $body, + 'timeout' => 5, + 'ignore_errors' => true, + ]]); + $raw = @file_get_contents($url, false, $ctx); + $httpCode = $raw !== false ? 200 : 0; + $curlErr = ''; + } + + $result['probe'] = [ + 'url' => $url, + 'http_code' => $httpCode, + 'curl_err' => $curlErr ?: null, + 'decoded' => json_decode((string)$raw, true), + ]; + $result['probe_raw'] = substr((string)$raw, 0, 1000); + +// ── Schema introspection — discover actual field names ──────────────────────── +if ($key) { + $types = ['InfoOs','InfoCpu','InfoMemory','ArrayDisk','ArrayParity','ArrayCache','Vm','Domain','VmDomain']; + $introspectGql = '{ ' . implode(' ', array_map(fn($t) => + "{$t}: __type(name: \"{$t}\") { fields { name type { name kind ofType { name kind } } } }", + $types + )) . ' }'; + + $body2 = json_encode(['query' => $introspectGql]); + $ch2 = curl_init('http://localhost/graphql'); + curl_setopt_array($ch2, [ + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/json', "x-api-key: {$key}"], + CURLOPT_POSTFIELDS => $body2, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 8, + ]); + $raw2 = curl_exec($ch2); + curl_close($ch2); + $result['schema'] = json_decode((string)$raw2, true)['data'] ?? null; + + // Pool drive names — what does the API actually return for cache/pool drives? + $ch_pools = curl_init('http://localhost/graphql'); + curl_setopt_array($ch_pools, [ + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/json', "x-api-key: {$key}"], + CURLOPT_POSTFIELDS => json_encode(['query' => '{ array { caches { name device type status fsType } } }']), + CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8, + ]); + $result['pool_drives'] = json_decode((string)curl_exec($ch_pools), true)['data'] ?? null; + curl_close($ch_pools); + + // Round 2: introspect CpuUtilization and MemoryUtilization field names + $types2 = ['CpuUtilization','MemoryUtilization','TemperatureMetrics']; + $gql2 = '{ ' . implode(' ', array_map(fn($t) => + "{$t}: __type(name: \"{$t}\") { kind fields { name type { name kind ofType { name kind } } } }", + $types2 + )) . ' }'; + $ch3 = curl_init('http://localhost/graphql'); + curl_setopt_array($ch3, [ + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/json', "x-api-key: {$key}"], + CURLOPT_POSTFIELDS => json_encode(['query' => $gql2]), + CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 8, + ]); + $result['schema2'] = json_decode((string)curl_exec($ch3), true)['data'] ?? null; + curl_close($ch3); +} +} + +echo json_encode($result, JSON_PRETTY_PRINT); diff --git a/Plugin/unraid/api/confform.php b/Plugin/unraid/api/confform.php index 4565eae..630f13a 100644 --- a/Plugin/unraid/api/confform.php +++ b/Plugin/unraid/api/confform.php @@ -36,7 +36,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } $results = vv_conf_write_changes($changes); - echo json_encode(['ok' => !in_array(false, $results, true), 'files' => $results]); + + // Propagate master.conf to partner hosts when the owner edits it (mirrors rawconf.php). + $push = []; + if (($results['master.conf'] ?? false) === true) { + $push = vv_push_master_conf(); + vv_push_setup_state(); + } + + echo json_encode(['ok' => !in_array(false, $results, true), 'files' => $results, 'push' => $push]); exit; } diff --git a/Plugin/unraid/api/create_api_key.php b/Plugin/unraid/api/create_api_key.php new file mode 100644 index 0000000..c738fef --- /dev/null +++ b/Plugin/unraid/api/create_api_key.php @@ -0,0 +1,84 @@ + false, 'error' => 'Cannot detect local host']); + exit; +} + +$hostUpper = strtoupper($host); +$varName = $hostUpper . '_UNRAID_API_KEY'; +$confFile = $host . '.conf'; + +// Create/overwrite the Varaverk API key. +// --description and --roles are required to suppress interactive prompts. +// --overwrite replaces any existing key with the same name (keeps it to one). +$dbg = ['ts' => date('H:i:s'), 'user' => trim(shell_exec('whoami'))]; +$output = shell_exec('timeout 10 /usr/local/sbin/unraid-api apikey --name "Varaverk" --create --overwrite --description "Varaverk plugin" --roles ADMIN --json &1'); +$dbg['raw'] = $output; +file_put_contents('/tmp/vv_apikey_debug.json', json_encode($dbg, JSON_PRETTY_PRINT)); + +if (!$output) { + echo json_encode(['ok' => false, 'error' => 'unraid-api returned no output — check /tmp/vv_apikey_debug.json']); + exit; +} + +$data = json_decode(trim($output), true); +if (!is_array($data)) { + echo json_encode(['ok' => false, 'error' => 'Could not parse unraid-api output', 'raw' => substr($output, 0, 300)]); + exit; +} + +$key = $data['key'] ?? null; +if (!$key) { + echo json_encode(['ok' => false, 'error' => 'No key in response', 'raw' => substr($output, 0, 300)]); + exit; +} + +// Read conf, replace the key value, write back +$raw = vv_read_conf_raw($confFile); +if ($raw === '') { + echo json_encode(['ok' => false, 'error' => 'Cannot read ' . $confFile]); + exit; +} + +// If line is missing (older conf created before this field was added to the template), +// insert it after HOST*_OWNER_EMAIL, or after HOST*_SSH_KEY, or append to file. +if (!str_contains($raw, $varName)) { + $inserted = false; + foreach ([$hostUpper . '_OWNER_EMAIL', $hostUpper . '_SSH_KEY'] as $anchor) { + if (str_contains($raw, $anchor)) { + $raw = preg_replace( + '/^(\s*' . preg_quote($anchor, '/') . '\s*=.*$)/m', + '$1' . "\n " . $varName . '=""', + $raw, 1 + ); + $inserted = true; + break; + } + } + if (!$inserted) { + $raw = rtrim($raw) . "\n " . $varName . '=""' . "\n"; + } +} + +// Replace quoted value in-place +$updated = preg_replace( + '/^(\s*' . preg_quote($varName, '/') . '\s*=\s*)"[^"]*"/m', + '${1}"' . $key . '"', + $raw +); + +if (!vv_write_conf_raw($confFile, $updated)) { + echo json_encode(['ok' => false, 'error' => 'Failed to write ' . $confFile]); + exit; +} + +echo json_encode([ + 'ok' => true, + 'key_preview' => substr($key, 0, 8) . '...' . substr($key, -4), + 'conf_file' => $confFile, +]); diff --git a/Plugin/unraid/api/flag_toggle.php b/Plugin/unraid/api/flag_toggle.php index ce3eb8a..9cdda4e 100644 --- a/Plugin/unraid/api/flag_toggle.php +++ b/Plugin/unraid/api/flag_toggle.php @@ -11,4 +11,12 @@ if (!$name || !preg_match('/^[A-Z_]+_RSYNC_ENABLED$/', $name)) { } $ok = vv_conf_flag_set($name, $enabled); -echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write master.conf']); + +// master.conf is shared — propagate the change to partner hosts (no-op on non-owner). +$push = []; +if ($ok) { + $push = vv_push_master_conf(); + vv_push_setup_state(); +} + +echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'Failed to write master.conf', 'push' => $push]); diff --git a/Plugin/unraid/api/partnership_ping.php b/Plugin/unraid/api/partnership_ping.php new file mode 100644 index 0000000..adcb43f --- /dev/null +++ b/Plugin/unraid/api/partnership_ping.php @@ -0,0 +1,14 @@ + false, 'error' => 'Invalid host id']); + exit; +} + +echo json_encode(vv_pt_ping($id)); diff --git a/Plugin/unraid/api/partnership_settings.php b/Plugin/unraid/api/partnership_settings.php new file mode 100644 index 0000000..e480084 --- /dev/null +++ b/Plugin/unraid/api/partnership_settings.php @@ -0,0 +1,22 @@ + stripos($g['subsection'], 'partnership') !== false + )); + if ($groups) { + $out[] = ['file' => $f, 'groups' => $groups]; + } +} + +echo json_encode(['ok' => true, 'files' => $out]); diff --git a/Plugin/unraid/api/setup.php b/Plugin/unraid/api/setup.php index b7f719b..d9648bd 100644 --- a/Plugin/unraid/api/setup.php +++ b/Plugin/unraid/api/setup.php @@ -50,7 +50,7 @@ if ($action === 'pull') { . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip; $remoteCfg = trim(shell_exec($sshBase . ' "grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null"') ?: ''); preg_match('/SCRIPTS_DIR\s*=\s*["\']?([^"\']+)["\']?/', $remoteCfg, $sm); - $remoteConf = rtrim($sm[1] ?? '/mnt/user/appdata/Varaverk', '/') . '/Configurations'; + $remoteConf = rtrim($sm[1] ?? '/boot/config/plugins/varaverk', '/') . '/Configurations'; // SCP master.conf from HOST1 $localMaster = CONF_DIR . '/master.conf'; diff --git a/Plugin/unraid/include/common.php b/Plugin/unraid/include/common.php index 7dcf3ed..6cd59b6 100644 --- a/Plugin/unraid/include/common.php +++ b/Plugin/unraid/include/common.php @@ -584,7 +584,17 @@ function vv_remote_hosts_stats(): array { if ($cached) { $results[$id] = $cached; continue; } } - $gql = '{ info { os { hostname uptime release } cpu { brand threads cores } } metrics { cpu { percentTotal } memory { percentTotal total available } } array { state } }'; + $gql = '{ + info { os { hostname uptime release } cpu { brand threads cores } } + metrics { cpu { percentTotal } memory { percentTotal total used available } } + array { + state + disks { fsSize fsUsed temp } + caches { fsSize fsUsed temp } + parities { temp } + } + vms { domains { name } } +}'; $data = vv_unraid_api_query(strtolower($id), $gql, 4, $key); if (!$data) { @@ -594,22 +604,17 @@ function vv_remote_hosts_stats(): array { continue; } - $os = $data['info']['os'] ?? []; - $cpu = $data['info']['cpu'] ?? []; - $metrics = $data['metrics'] ?? []; - $mCpu = $metrics['cpu'] ?? []; - $mMem = $metrics['memory'] ?? []; - $arr = $data['array'] ?? []; + $os = $data['info']['os'] ?? []; + $cpu = $data['info']['cpu'] ?? []; + $mMem = $data['metrics']['memory'] ?? []; - $cpuLoad = round((float)($mCpu['percentTotal'] ?? 0), 1); - $memPct = round((float)($mMem['percentTotal'] ?? 0)); - // Also compute from raw bytes as cross-check when percentTotal is missing + $memPct = round((float)($mMem['percentTotal'] ?? 0)); if ($memPct === 0) { $totalBytes = (float)($mMem['total'] ?? 0); $availBytes = (float)($mMem['available'] ?? 0); $memPct = $totalBytes > 0 ? (int)round(($totalBytes - $availBytes) / $totalBytes * 100) : 0; } - $memTotalGb = isset($mMem['total']) ? round((float)$mMem['total'] / (1024 ** 3), 1) : 0; + $memTotalGb = isset($mMem['total']) ? _vv_api_bytes_to_gb((float)$mMem['total']) : 0; $uptimeRaw = $os['uptime'] ?? ''; if (is_numeric($uptimeRaw)) { @@ -623,19 +628,20 @@ function vv_remote_hosts_stats(): array { $uptime = $uptimeRaw ?: '—'; } - $entry = [ + $nodeMetrics = vv_api_node_metrics($data); + $entry = array_merge([ 'available' => true, 'host_id' => $id, 'hostname' => $os['hostname'] ?? $vars[$id], 'version' => $os['release'] ?? '', 'uptime' => $uptime, 'uptime_sec' => $uptimeSec, - 'cpu_load' => $cpuLoad, - 'cpu_threads' => (int)($cpu['threads'] ?? 0), + 'cpu_load' => $nodeMetrics['cpu_pct'] ?? 0, + 'cpu_threads' => (int)($cpu['threads'] ?? 0), 'mem_total_gb' => $memTotalGb, 'mem_used_pct' => $memPct, - 'array_state' => $arr['state'] ?? 'UNKNOWN', - ]; + 'array_state' => $data['array']['state'] ?? 'UNKNOWN', + ], $nodeMetrics); file_put_contents($cacheFile, json_encode($entry)); $results[$id] = $entry; } diff --git a/Plugin/unraid/include/confform.php b/Plugin/unraid/include/confform.php index 4a4d5f2..f1cc720 100644 --- a/Plugin/unraid/include/confform.php +++ b/Plugin/unraid/include/confform.php @@ -67,6 +67,12 @@ function vv_conf_parse_subsection(string $raw, string $subName, string $filename if (preg_match('/^#\s*[━─═=]{3,}/', $lines[$i])) { $end = $i; break; } } + return _vv_conf_parse_field_range($lines, $start, $end, $filename) ?: null; +} + +// Parse all config fields between two line indices. Shared by vv_conf_parse_subsection() +// (per-script editor) and vv_conf_all_groups() (full settings view). +function _vv_conf_parse_field_range(array $lines, int $start, int $end, string $filename): array { $fields = []; $pendingDesc = []; @@ -138,7 +144,36 @@ function vv_conf_parse_subsection(string $raw, string $subName, string $filename } } - return $fields ?: null; + return $fields; +} + +// Return ALL config groups (every named section + its fields) for a conf file. +// Enumerates header lines (# ━━━ Name ━━━ or # ── Name ──); each group runs from its +// header to the next named header so major sections (sandwiched in ===) capture their +// settings too. Empty groups (divider-only headers) are dropped. +function vv_conf_all_groups(string $filename): array { + $raw = vv_read_conf_raw($filename); + if ($raw === '') return []; + $lines = explode("\n", $raw); + $n = count($lines); + + $headers = []; + for ($i = 0; $i < $n; $i++) { + if (preg_match('/^#\s*[━─]{2,}\s+([A-Za-z].+?)\s+[━─]{2,}/', $lines[$i], $m)) { + $headers[] = ['name' => trim(preg_replace('/\s+/', ' ', $m[1])), 'line' => $i]; + } + } + + $groups = []; + foreach ($headers as $idx => $h) { + $start = $h['line'] + 1; + $end = $headers[$idx + 1]['line'] ?? $n; + $fields = _vv_conf_parse_field_range($lines, $start, $end, $filename); + if ($fields) { + $groups[] = ['subsection' => $h['name'], 'file' => $filename, 'fields' => $fields]; + } + } + return $groups; } // Return all conf groups (subsection + fields) for a script on the current host. diff --git a/Plugin/unraid/include/config.php b/Plugin/unraid/include/config.php index f8bce97..6910e2d 100644 --- a/Plugin/unraid/include/config.php +++ b/Plugin/unraid/include/config.php @@ -5,12 +5,14 @@ define('PLUGIN_CFG', '/boot/config/plugins/varaverk/varaverk.cfg'); $_vv_cfg = @parse_ini_file(PLUGIN_CFG) ?: []; -define('SCRIPTS_DIR', $_vv_cfg['SCRIPTS_DIR'] ?? '/mnt/user/appdata/Varaverk'); +define('SCRIPTS_DIR', $_vv_cfg['SCRIPTS_DIR'] ?? '/boot/config/plugins/varaverk'); define('CONF_DIR', SCRIPTS_DIR . '/Configurations'); +define('DATA_DIR', SCRIPTS_DIR . '/data'); +define('STATE_DIR', SCRIPTS_DIR . '/State_Files'); define('LOG_DIR', '/var/log/varaverk'); unset($_vv_cfg); -const VV_SETUP_STATE_FILE = '/boot/config/varaverk_setup.db'; +define('VV_SETUP_STATE_FILE', STATE_DIR . '/varaverk_setup.db'); // Read the setup state file into a key=>value array. function vv_setup_state_read(): array { diff --git a/Plugin/unraid/include/monitor.php b/Plugin/unraid/include/monitor.php index bd39592..a7c9079 100644 --- a/Plugin/unraid/include/monitor.php +++ b/Plugin/unraid/include/monitor.php @@ -156,7 +156,7 @@ function vv_watchdog_summary(): array { } // Recent restarts (24 h) - $restartLog = '/mnt/user/appdata/Varaverk/data/container_restart_history.db'; + $restartLog = DATA_DIR . '/container_restart_history.db'; $restartRaw = @file_get_contents($restartLog) ?: ''; $cutoff = time() - 86400; $restarts = []; @@ -297,6 +297,7 @@ function vv_scripts_status(): array { $ts = (int)($stat['end'] ?? $stat['start'] ?? @filemtime($statFile) ?: 0); $scripts[] = [ + 'id' => $id, 'name' => $name, 'last_ts' => $ts, 'status' => $status, diff --git a/Plugin/unraid/include/partnership.php b/Plugin/unraid/include/partnership.php index 791b143..ccfdc30 100644 --- a/Plugin/unraid/include/partnership.php +++ b/Plugin/unraid/include/partnership.php @@ -3,23 +3,63 @@ require_once __DIR__ . '/config.php'; require_once __DIR__ . '/arrs.php'; // vv_arr_known_hosts(), vv_arr_scalar() +require_once __DIR__ . '/common.php'; // vv_system_info(), vv_docker_containers(), vv_remote_hosts_stats(), vv_api_node_metrics() // ── Config ──────────────────────────────────────────────────────────────────── function vv_pt_config(): array { $v = vv_conf_vars(); + $offlineDays = null; + $odFile = '/boot/config/partnership_offline_days.db'; + if (file_exists($odFile)) { + $raw = trim(@file_get_contents($odFile) ?: ''); + if (is_numeric($raw)) $offlineDays = (int)$raw; + } return [ 'enabled' => ($v['PARTNERSHIP_ENABLED'] ?? 'false') === 'true', 'owner_host' => $v['PARTNERSHIP_OWNER_HOST'] ?? '', 'sync_min' => (int)($v['PARTNERSHIP_SYNC_INTERVAL'] ?? 15), 'grace_hours' => (int)($v['PARTNERSHIP_GRACE_HOURS'] ?? 6), 'offline_threshold' => (int)($v['PARTNERSHIP_OFFLINE_THRESHOLD'] ?? 30), + 'offline_days' => $offlineDays, 'remove_tailscale' => ($v['PARTNERSHIP_REMOVE_TAILSCALE'] ?? 'true') === 'true', 'folderview3' => ($v['PARTNERSHIP_FOLDERVIEW3'] ?? 'false') === 'true', 'tailscale_configured' => !empty($v['TAILSCALE_API_KEY']) && !empty($v['TAILSCALE_TAILNET']), + 'transfer_confirm' => $v['PARTNERSHIP_TRANSFER_CONFIRM'] ?? 'i-understand-this-transfers-ownership', ]; } +// ── Mirror sync health — the partnership's actual job (rsync orchestrators) ────── +function vv_pt_sync(): array { + $jobs = [ + 'critical' => 'Orchestrators/critical_sync_maintenance', + 'daily' => 'Orchestrators/daily_sync_maintenance', + 'weekly' => 'Orchestrators/weekly_sync_maintenance', + ]; + $out = ['jobs' => []]; + foreach ($jobs as $key => $base) { + $statFile = LOG_DIR . '/' . $base . '.json'; + $s = file_exists($statFile) ? json_decode(@file_get_contents($statFile), true) : null; + $out['jobs'][$key] = is_array($s) ? [ + 'status' => $s['status'] ?? 'unknown', + 'start' => isset($s['start']) ? (int)$s['start'] : null, + 'end' => isset($s['end']) ? (int)$s['end'] : null, + ] : null; + } + $v = vv_conf_vars(); + // Rsync gate flags (master.conf) — global Tier 1 + per-tier Tier 2. + $out['gates'] = [ + 'global' => ['var' => 'RSYNC_ENABLED', 'on' => ($v['RSYNC_ENABLED'] ?? 'true') === 'true'], + 'critical' => ['var' => 'CRITICAL_RSYNC_ENABLED', 'on' => ($v['CRITICAL_RSYNC_ENABLED'] ?? 'true') === 'true'], + 'daily' => ['var' => 'DAILY_RSYNC_ENABLED', 'on' => ($v['DAILY_RSYNC_ENABLED'] ?? 'true') === 'true'], + 'weekly' => ['var' => 'WEEKLY_RSYNC_ENABLED', 'on' => ($v['WEEKLY_RSYNC_ENABLED'] ?? 'true') === 'true'], + ]; + // Back-compat keys still used by the warning line. + $out['rsync_enabled'] = $out['gates']['global']['on']; + $out['critical_enabled'] = $out['gates']['critical']['on']; + return $out; +} + // ── State file parser ───────────────────────────────────────────────────────── function vv_pt_read_db(string $path): array { @@ -76,28 +116,44 @@ function vv_pt_ssh(string $ip, string $sshKey, string $cmd, int $timeout = 4): s // ── System info ─────────────────────────────────────────────────────────────── +// vv_system_info() (common.php) provides version, load_avg, array_state. +// /proc/uptime is the reliable uptime source (API uptime is an ISO date string, not seconds). +// vv_docker_containers() (common.php) provides the running container list. function vv_pt_local_system(): array { - $ver = ''; - if (file_exists('/etc/unraid-version')) { - preg_match('/VERSION="([^"]+)"/', file_get_contents('/etc/unraid-version'), $m); - $ver = $m[1] ?? ''; - } - $uptime = 0; - if (file_exists('/proc/uptime')) { - $uptime = (int)explode(' ', file_get_contents('/proc/uptime'))[0]; - } - return ['unraid_version' => $ver, 'uptime_sec' => $uptime]; + $info = vv_system_info(); + $uptimeSec = file_exists('/proc/uptime') + ? (int)explode(' ', file_get_contents('/proc/uptime'))[0] : 0; + return [ + 'unraid_version' => $info['version'] ?? '', + 'uptime_sec' => $uptimeSec, + 'load_avg' => isset($info['load_avg']) ? $info['load_avg'][0] : null, + 'containers' => count(vv_docker_containers()), + ]; } +// SSH fallback for remote nodes — version/uptime/load/containers in one call. +// API stats from vv_remote_hosts_stats() take priority when available; SSH fills gaps. function vv_pt_remote_system(string $ip, string $sshKey): array { $out = vv_pt_ssh($ip, $sshKey, - 'printf "%s\nUPTIME:%s\n" "$(cat /etc/unraid-version 2>/dev/null)" "$(cat /proc/uptime 2>/dev/null)"'); + 'printf "%s\nUPTIME:%s\nLOAD:%s\nCONTAINERS:%s\n" ' . + '"$(cat /etc/unraid-version 2>/dev/null)" ' . + '"$(cat /proc/uptime 2>/dev/null)" ' . + '"$(awk \'{print $1}\' /proc/loadavg 2>/dev/null)" ' . + '"$(docker ps -q 2>/dev/null | wc -l)"'); $ver = ''; - preg_match('/VERSION="([^"]+)"/', $out, $m); - if ($m) $ver = $m[1]; + preg_match('/VERSION="([^"]+)"/', $out, $m); if ($m) $ver = $m[1]; $uptime = 0; if (preg_match('/UPTIME:([\d.]+)/', $out, $m)) $uptime = (int)$m[1]; - return ['unraid_version' => $ver, 'uptime_sec' => $uptime]; + $load = null; + if (preg_match('/LOAD:([\d.]+)/', $out, $m)) $load = round((float)$m[1], 2); + $containers = null; + if (preg_match('/CONTAINERS:(\d+)/', $out, $m)) $containers = (int)$m[1]; + return [ + 'unraid_version' => $ver, + 'uptime_sec' => $uptime, + 'load_avg' => $load, + 'containers' => $containers, + ]; } // ── Per-node data ───────────────────────────────────────────────────────────── @@ -110,6 +166,9 @@ function vv_pt_nodes(): array { $ownerSlot = strtolower($vars['PARTNERSHIP_OWNER_HOST'] ?? ''); $setupDb = vv_setup_state_read(); + // Remote host stats (API + 30s /tmp cache) — includes version, uptime, cpu/ram/array/temp/vms + $remoteStats = vv_remote_hosts_stats(); + // SSH key for this host $myId = strtoupper($currentHost); $myRaw = vv_read_conf_raw($currentHost . '.conf'); @@ -161,32 +220,107 @@ function vv_pt_nodes(): array { // For self: local setup complete flag (set by partnership_manager --onboard --local-only) $localDone = $isMe && ($setupDb[$nodeIdUpper . '_LOCAL_DONE'] ?? '') === 'true'; + // Unraid API key status — checks Unraid's key store directly so deletions are reflected. + $apiKeySet = false; + $apiKeyPreview = ''; + if ($isMe) { + $apiOut = shell_exec('/usr/local/sbin/unraid-api apikey --name "Varaverk" --json /dev/null'); + $apiData = json_decode(trim($apiOut ?? ''), true); + if (is_array($apiData) && !empty($apiData['key'])) { + $apiKeySet = true; + $apiKeyPreview = substr($apiData['key'], 0, 8) . '...' . substr($apiData['key'], -4); + } + } + + // Live metrics: local uses vv_api_data() (cached); remote uses vv_remote_hosts_stats() (30s cache) + if ($isMe) { + $metrics = array_merge( + vv_api_node_metrics(vv_api_data()), + array_filter([ + 'load_avg' => $system['load_avg'] ?? null, + 'containers' => $system['containers'] ?? null, + ], fn($v) => $v !== null) + ); + } else { + $rStat = $remoteStats[$nodeIdUpper] ?? []; + // Merge API metrics from remote stats with SSH extras (load, containers) + $metrics = array_filter([ + 'cpu_pct' => $rStat['cpu_pct'] ?? null, + 'ram_used_gb' => $rStat['ram_used_gb'] ?? null, + 'ram_total_gb' => $rStat['ram_total_gb'] ?? null, + 'array_used_tb' => $rStat['array_used_tb'] ?? null, + 'array_total_tb' => $rStat['array_total_tb'] ?? null, + 'max_disk_temp' => $rStat['max_disk_temp'] ?? null, + 'vm_count' => $rStat['vm_count'] ?? null, + 'load_avg' => $system['load_avg'] ?? null, + 'containers' => $system['containers'] ?? null, + ], fn($v) => $v !== null); + // Fill version/uptime from API stats if SSH didn't provide them + if (empty($system['unraid_version']) && !empty($rStat['version'])) { + $system['unraid_version'] = $rStat['version']; + } + if (empty($system['uptime_sec']) && !empty($rStat['uptime_sec'])) { + $system['uptime_sec'] = $rStat['uptime_sec']; + } + } + $nodes[] = [ - 'slot' => $slot, - 'id' => $nodeIdUpper, - 'hostname' => $hostname, - 'is_me' => $isMe, - 'is_owner' => $isOwner, - 'ts_online' => $ts['online'], - 'ts_active' => $ts['active'], - 'ts_ip' => $ts['ip'], - 'fallback' => $fbState, - 'partnership' => $ptDb, - 'system' => $system, - 'onboard_phase' => $onboardPhase, - 'key_ready' => $keyReady, - 'local_done' => $localDone, + 'slot' => $slot, + 'id' => $nodeIdUpper, + 'hostname' => $hostname, + 'is_me' => $isMe, + 'is_owner' => $isOwner, + 'ts_online' => $ts['online'], + 'ts_active' => $ts['active'], + 'ts_ip' => $ts['ip'], + 'fallback' => $fbState, + 'partnership' => $ptDb, + 'system' => $system, + 'onboard_phase' => $onboardPhase, + 'key_ready' => $keyReady, + 'local_done' => $localDone, + 'api_key_set' => $apiKeySet, + 'api_key_preview' => $apiKeyPreview, + 'metrics' => $metrics, ]; } return $nodes; } +// ── Connectivity test — SSH echo with round-trip timing ───────────────────────── +function vv_pt_ping(string $slot): array { + $slot = strtolower($slot); + $vars = vv_conf_vars(); + $hostname = $vars[strtoupper($slot)] ?? ''; + if (!$hostname) return ['ok' => false, 'error' => 'Unknown host slot']; + + $currentHost = vv_detect_host(); + $myRaw = vv_read_conf_raw($currentHost . '.conf'); + $sshKey = vv_arr_scalar($myRaw, strtoupper($currentHost) . '_SSH_KEY'); + if (!$sshKey || !file_exists($sshKey)) { + return ['ok' => false, 'error' => 'No SSH key configured on this host']; + } + + $ip = vv_resolve_tailscale_ip($hostname); + if (!$ip) return ['ok' => false, 'error' => "Cannot resolve Tailscale IP for $hostname"]; + + $t0 = microtime(true); + $out = vv_pt_ssh($ip, $sshKey, 'echo ok', 8); + $ms = (int)round((microtime(true) - $t0) * 1000); + + if (trim($out) === 'ok') { + return ['ok' => true, 'latency_ms' => $ms, 'host' => $hostname, 'ip' => $ip]; + } + return ['ok' => false, 'error' => "SSH to $hostname ($ip) failed or timed out", 'host' => $hostname]; +} + // ── Entry point ─────────────────────────────────────────────────────────────── function vv_partnership_all(): array { return [ 'config' => vv_pt_config(), 'nodes' => vv_pt_nodes(), + 'sync' => vv_pt_sync(), 'ts' => time(), ]; } diff --git a/Plugin/unraid/include/unraid_api.php b/Plugin/unraid/include/unraid_api.php index 0409b7f..8e2c27f 100644 --- a/Plugin/unraid/include/unraid_api.php +++ b/Plugin/unraid/include/unraid_api.php @@ -142,6 +142,44 @@ function vv_api_disk_entry(array $d, string $role = ''): ?array { ]; } +// ── Node metrics extractor ──────────────────────────────────────────────────── + +// Parse CPU%, RAM, array storage, disk temps, and VM count from a raw API response. +// Used by vv_remote_hosts_stats() and the local vv_api_data() path — one parser, no duplication. +// GQL must include: metrics.cpu.percentTotal, metrics.memory.{total,used}, +// array.{disks,caches,parities}.{fsSize,fsUsed,temp}, vms.domains. +function vv_api_node_metrics(?array $d): array { + if (!$d) return []; + $cpu = (int)round((float)($d['metrics']['cpu']['percentTotal'] ?? 0)); + $mem = $d['metrics']['memory'] ?? []; + $ramUsed = isset($mem['used']) ? _vv_api_bytes_to_gb((float)$mem['used']) : null; + $ramTot = isset($mem['total']) ? _vv_api_bytes_to_gb((float)$mem['total']) : null; + + $disks = $d['array']['disks'] ?? []; + $caches = $d['array']['caches'] ?? []; + $pars = $d['array']['parities'] ?? []; + $usedGb = 0.0; $totGb = 0.0; + foreach (array_merge($disks, $caches) as $dk) { + $sz = (float)($dk['fsSize'] ?? 0); + if ($sz <= 0) continue; + $totGb += _vv_api_bytes_to_gb($sz); + $usedGb += _vv_api_bytes_to_gb((float)($dk['fsUsed'] ?? 0)); + } + $temps = array_filter( + array_merge(array_column($disks,'temp'), array_column($caches,'temp'), array_column($pars,'temp')), + fn($t) => is_numeric($t) && $t > 0 + ); + return [ + 'cpu_pct' => $cpu, + 'ram_used_gb' => $ramUsed !== null ? round($ramUsed, 1) : null, + 'ram_total_gb' => $ramTot !== null ? round($ramTot, 1) : null, + 'array_used_tb' => $totGb > 0 ? round($usedGb / 1000, 1) : null, + 'array_total_tb' => $totGb > 0 ? round($totGb / 1000, 1) : null, + 'max_disk_temp' => $temps ? (int)max($temps) : null, + 'vm_count' => count($d['vms']['domains'] ?? []), + ]; +} + // ── Confirmed schema (Unraid 7.2.5, introspected 2026-05-29) ───────────────── // Adding a new host: add HOSTn="hostname" to master.conf and HOSTn_UNRAID_API_KEY // to hostn.conf, then run Deployment/deploy.sh. No schema work needed. diff --git a/Plugin/unraid/include/watchdog.php b/Plugin/unraid/include/watchdog.php index 8ed19c1..217577c 100644 --- a/Plugin/unraid/include/watchdog.php +++ b/Plugin/unraid/include/watchdog.php @@ -313,7 +313,7 @@ function vv_wd_all(): array { $currentHost = vv_detect_host(); $tsPeers = vv_pt_ts_peers(); $masterRaw = vv_read_conf_raw('master.conf'); - $restartLog = '/mnt/user/appdata/Varaverk/data/container_restart_history.db'; + $restartLog = DATA_DIR . '/container_restart_history.db'; // Config thresholds from master.conf $cfg = [ diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php index 6ee44c2..ef2ab91 100644 --- a/Plugin/unraid/pages/monitor.php +++ b/Plugin/unraid/pages/monitor.php @@ -467,7 +467,10 @@ function vvRenderScripts() { const color = s.status === 'running' ? '#4fc3f7' : s.status === 'ok' ? '#4caf50' : s.status === 'warn' ? '#ff9800' : s.status === 'error' ? '#f44336' : '#555'; const name = s.name.length > 24 ? s.name.slice(0, 23) + '…' : s.name; const dur = s.duration != null ? ` ${s.duration}s` : ''; - listHtml += `
+ const sid = (s.id || '').replace(/\\/g,'\\\\').replace(/'/g,"\\'"); + listHtml += `
${icon} ${name} ${ago}${dur} @@ -485,6 +488,12 @@ function vvScriptsFilterSet(type) { vvRenderScripts(); } +// Deep-link a script row to the Scheduler tab (opens its settings/log panel there). +function vvOpenScript(id) { + if (!id) { window.location.href = '?tab=scheduler'; return; } + window.location.href = '?tab=scheduler&vv_script=' + encodeURIComponent(id); +} + // ── Disk / storage helpers (module-level so vvRenderPools can call them) ───── function vvTempColor(tempC, transport) { @@ -575,6 +584,16 @@ function vvPollMonitor() { const _runningVMs = (d.vms?.vms ?? []).filter(v => v.state === 'running').length; const _threadInfo = sys.cpu_threads ? `${sys.cpu_cores}c / ${sys.cpu_threads}t` : ''; + // Load average (1/5/15m) — colour by load[0] vs core count + const _load = Array.isArray(sys.load_avg) ? sys.load_avg : null; + const _cores = sys.cpu_cores || 0; + const _loadColor = _load && _cores + ? (_load[0] > _cores * 2 ? '#f44336' : _load[0] > _cores ? '#ff9800' : '#888') : '#888'; + const _loadStr = _load + ? `${_load[0].toFixed(2)} · ${_load[1].toFixed(2)} · ${_load[2].toFixed(2)}` + : '—'; + const _coreMeta = _threadInfo ? ` (${_threadInfo})` : ''; + document.getElementById('vv-system-body').innerHTML = `
@@ -608,9 +627,10 @@ function vvPollMonitor() {
${timeStr}
${dateStr} · ${tz}
- Model ${sys.cpu_model}${_threadInfo ? ` (${_threadInfo})` : ''} + Model ${sys.cpu_model}${_coreMeta} Array ${sys.array_state} Uptime ${sys.uptime} + Load ${_loadStr} Running ${_runningCtrs} ctr${_runningCtrs !== 1 ? 's' : ''}${_runningVMs > 0 ? ` · ${_runningVMs} VM` : ''} Version ${ver}
`; @@ -856,6 +876,8 @@ function vvPollMonitor() { const maxSeen = Math.max(...vvNetRxHistory, ...vvNetTxHistory, 1); const maxBps = maxSeen * 1.25; // auto-scale with 25% headroom + const peakRx = Math.max(...vvNetRxHistory, 0); // window peak (last 2 min) + const peakTx = Math.max(...vvNetTxHistory, 0); const ipRows = [ net.local_ip ? `
LAN  ${net.local_ip}
` : '', @@ -868,8 +890,8 @@ function vvPollMonitor() {
${net.iface}  ·  ${linkLabel}
- ━ IN (RX)${vvFmtBps(rx)} - ━ OUT (TX)${vvFmtBps(tx)} + ━ IN (RX)${vvFmtBps(rx)}peak ${vvFmtBps(peakRx)} + ━ OUT (TX)${vvFmtBps(tx)}peak ${vvFmtBps(peakTx)}
${ipRows}
@@ -881,9 +903,15 @@ function vvPollMonitor() { document.getElementById('vv-network-body').innerHTML = '

No network interface detected

'; } - // ── CPU title ──────────────────────────────────────────────────────────── + // ── CPU title (core count + temp to its right) ──────────────────────────── const _cpuTitleEl = document.getElementById('vv-cpu-title'); - if (_cpuTitleEl && sys.cpu_threads) _cpuTitleEl.textContent = `CPU · ${sys.cpu_cores}c/${sys.cpu_threads}t`; + if (_cpuTitleEl) { + const _cpuTemp = d.watchdog?.stability?.cpu_temp ?? null; + const _tColor = _cpuTemp == null ? '#888' : _cpuTemp >= 88 ? '#f44336' : _cpuTemp >= 75 ? '#ff9800' : '#4caf50'; + const _coreLbl = sys.cpu_threads ? `CPU · ${sys.cpu_cores}c/${sys.cpu_threads}t` : 'CPU'; + _cpuTitleEl.innerHTML = _coreLbl + + (_cpuTemp != null ? ` ${_cpuTemp}°` : ''); + } // ── UPS / Power ───────────────────────────────────────────────────────── const ups = d.ups ?? {}; @@ -1072,13 +1100,15 @@ function vvPollMonitor() { const sshdOk = stab.sshd_ok ?? true; const zombies = stab.zombies ?? 0; - let statsHtml = `
+ const cpuRow = stab.cpu_temp != null + ? `CPU${stab.cpu_temp}°C` : ''; + let statsHtml = `
rootfs${stab.rootfs_pct??0}% /var/log${stab.log_pct??0}% /tmp${stab.tmp_pct??0}% RAM free${ramFree}GB Load${load} - ${stab.cpu_temp != null ? `CPU${stab.cpu_temp}°C` : ''} + ${cpuRow} Zombies${zombies} ${stab.nic??'nic'}● ${stab.nic_state??'?'} sshd${sshdOk?'● ok':'✗ down'} @@ -1208,6 +1238,22 @@ function vvPollMonitor() { const procCount = gpuProcs.length; const procColor = procCount > 0 ? '#4caf50' : '#555'; + // Process list — which apps are actually using the GPU (name + VRAM) + let gpuProcHtml = ''; + if (procCount > 0) { + const rows = gpuProcs.map(p => { + const pname = (p.name || '').split('/').pop() || p.name || 'proc'; + return `
+ ${pname} + ${p.memory_mb} MB +
`; + }).join(''); + gpuProcHtml = `
+
GPU processes
+
${rows}
+
`; + } + document.getElementById('vv-gpu-body').innerHTML = // header row: name + process count pill `
@@ -1224,7 +1270,7 @@ function vvPollMonitor() { ${temp}°C Power ${powerStr} -
`; +
` + gpuProcHtml; } else { document.getElementById('vv-gpu-body').innerHTML = '

No GPU detected

'; } diff --git a/Plugin/unraid/pages/partnership.php b/Plugin/unraid/pages/partnership.php index 643ef9f..a1c30c3 100644 --- a/Plugin/unraid/pages/partnership.php +++ b/Plugin/unraid/pages/partnership.php @@ -32,6 +32,28 @@ 0%,100% { box-shadow: 0 0 0 2px #4a8, 0 0 8px rgba(100,200,120,.2); } 50% { box-shadow: 0 0 0 4px #4a8, 0 0 18px rgba(100,200,120,.45); } } + +/* ── Settings panel ─────────────────────────────────────────── */ +.vv-set-file { border:1px solid #222; border-radius:5px; margin-bottom:8px; overflow:hidden; } +.vv-set-file-hdr { display:flex; justify-content:space-between; align-items:center; padding:8px 12px; + background:#161616; cursor:pointer; font-size:12px; font-weight:bold; color:#bbb; font-family:monospace; } +.vv-set-file-hdr:hover { background:#1a1a1a; } +.vv-set-file-body { padding:6px 10px 10px; } +.vv-set-sec { border-top:1px solid #1c1c1c; } +.vv-set-sec:first-child { border-top:none; } +.vv-set-sec-hdr { display:flex; justify-content:space-between; align-items:center; padding:6px 4px; + cursor:pointer; font-size:11px; color:#888; text-transform:uppercase; letter-spacing:.04em; } +.vv-set-sec-hdr:hover { color:#ccc; } +.vv-set-sec-body { padding:4px 0 8px 8px; } +.vv-set-chev { color:#444; font-size:10px; } +.vv-set-field { margin-bottom:10px; } +.vv-set-key { font-size:11px; color:#7ab; font-family:monospace; margin-bottom:2px; } +.vv-set-desc { font-size:10px; color:#555; margin-bottom:3px; line-height:1.4; } +.vv-set-input { width:100%; box-sizing:border-box; background:#0d0d0d; border:1px solid #2a2a2a; + color:#ddd; padding:5px 8px; border-radius:3px; font-family:monospace; font-size:11px; } +.vv-set-input:focus { outline:none; border-color:#4a8; } +.vv-set-input.changed { border-color:#ff9800; } +textarea.vv-set-input { resize:vertical; white-space:pre; }
@@ -44,17 +66,46 @@
Loading…
+ + +
Loading…
+ +
+

Mirror Sync

+
Loading…
+
+

Actions

Loading…
+ +
+
+

Settings

+
+ + ▸ Show settings +
+
+
+ Partnership configuration for this server. +
+ +
+ + + + + +
diff --git a/Plugin/varaverk.plg b/Plugin/varaverk.plg index 4399728..ce4add2 100644 --- a/Plugin/varaverk.plg +++ b/Plugin/varaverk.plg @@ -2,18 +2,26 @@ - + + + ]> +###2026.05.31 +- Packaged release: web files now ship as a .txz that Unraid reinstalls to RAM on every boot +- Survives reboots with zero manual steps (no symlink, no go script) — fixes plugin vanishing after OS upgrades +- Scripts are git-cloned to appdata on first install; web files stay on flash (~200KB) +- Updates handled in-UI (git pull); the plugin no longer pulls on every boot + ###2026.05.30 - First-run setup wizard: auto-detects hostname, creates master.conf + host conf from templates - Scheduler setup mode: after wizard, master.conf and host conf open sequentially with forced save flow @@ -33,143 +41,153 @@ - + +https://github.com/FailedProxy/Varaverk/releases/download/&version;/&pkg; +&sha256; + + + + /dev/null) - [[ -n "$_sd" ]] && SCRIPTS_DIR="$_sd" -fi -SCRIPTS_DIR="${SCRIPTS_DIR:-/mnt/user/appdata/Varaverk}" +# Scripts live in the plugin dir on flash — no array needed. +SCRIPTS_DIR="$CFG_DIR" CONF_DIR="$SCRIPTS_DIR/Configurations" -# Array must be started — appdata must be available -if ! df --output=fstype /mnt/user 2>/dev/null | grep -q 'shfs'; then - log "Array not started — cannot install to appdata" - log "Start the array then reinstall, or wait for next boot" - exit 0 -fi - -# Clone or update repo -if [[ ! -d "$SCRIPTS_DIR/.git" ]]; then - log "Cloning $GITHUB ($BRANCH) → $SCRIPTS_DIR" - if ! git clone --branch "$BRANCH" "$GITHUB" "$SCRIPTS_DIR" >> "$LOG" 2>&1; then - log "ERROR: git clone failed — check internet access and GitHub URL" - exit 1 - fi - log "Clone complete" -else - log "Updating repo in $SCRIPTS_DIR" - git -C "$SCRIPTS_DIR" pull --ff-only origin "$BRANCH" >> "$LOG" 2>&1 \ - && log "Pull complete" \ - || log "WARNING: git pull failed — repo may have local modifications" -fi - -# Sanity check — Plugin/unraid must exist after clone -if [[ ! -d "$SCRIPTS_DIR/Plugin/unraid" ]]; then - log "ERROR: Plugin/unraid not found in cloned repo — unexpected repo structure" - exit 1 -fi - -# Symlink web files -if [[ -L "$WEB_DIR" ]]; then - rm "$WEB_DIR" -elif [[ -d "$WEB_DIR" ]]; then - log "WARNING: $WEB_DIR is a real directory — not replacing (remove manually if needed)" -fi -if [[ ! -e "$WEB_DIR" ]]; then - ln -s "$SCRIPTS_DIR/Plugin/unraid" "$WEB_DIR" - log "Symlinked: $WEB_DIR → $SCRIPTS_DIR/Plugin/unraid" -fi - -# Write varaverk.cfg if not present +# Seed varaverk.cfg with defaults (SCRIPTS_DIR + Gitea settings) if not present. if [[ ! -f "$CFG_FILE" ]]; then - printf 'SCRIPTS_DIR="%s"\n' "$SCRIPTS_DIR" > "$CFG_FILE" - log "Created varaverk.cfg (SCRIPTS_DIR=$SCRIPTS_DIR)" + cat > "$CFG_FILE" <<'CFGEOF' +SCRIPTS_DIR="/boot/config/plugins/varaverk" +GITEA_CONTAINER="Gitea" +GITEA_REPO_PATH="FailedProxy/Varaverk.git" +GITEA_SSH_KEY="/root/.ssh/unraid_gitea" +SSH_PORT="221" +CFGEOF + log "seeded varaverk.cfg" fi -# Create Configurations dir if git clone didn't -mkdir -p "$CONF_DIR" +# Read Gitea settings from varaverk.cfg (allows override without editing .plg). +_read_cfg() { grep -oP "(?<=^${1}=\")[^\"]*" "$CFG_FILE" 2>/dev/null || echo "${2}"; } +GITEA_CONTAINER=$(_read_cfg GITEA_CONTAINER "Gitea") +GITEA_REPO_PATH=$(_read_cfg GITEA_REPO_PATH "FailedProxy/Varaverk.git") +GITEA_SSH_KEY=$(_read_cfg GITEA_SSH_KEY "/root/.ssh/unraid_gitea") +SSH_PORT=$(_read_cfg SSH_PORT "221") -# Bootstrap master.conf from template on first install -if [[ ! -f "$CONF_DIR/master.conf" ]]; then - if [[ -f "$SCRIPTS_DIR/Configurations/master.conf.template" ]]; then - cp "$SCRIPTS_DIR/Configurations/master.conf.template" "$CONF_DIR/master.conf" - log "Created master.conf from template — fill in HOST1 and HOST2 to continue" - else - log "WARNING: master.conf.template not found — master.conf not created" +# Clone on first install only; never auto-pull (updates via the UI git pull). +if [[ ! -d "$SCRIPTS_DIR/.git" ]]; then + log "initialising repo in $SCRIPTS_DIR ($BRANCH)..." + + # Locate Gitea: local container → local IP; else Tailscale; else fall back to GitHub. + GITEA_IP="" + if command -v docker >/dev/null 2>&1 && \ + docker ps --format "{{.Names}}" 2>/dev/null | grep -q "^${GITEA_CONTAINER}$"; then + GITEA_IP=$(hostname -I | awk '{print $1}') + log "Gitea running locally — using $GITEA_IP" + elif command -v tailscale >/dev/null 2>&1; then + # Try each known peer until we find one hosting Gitea + while IFS= read -r peer_ip; do + if ssh -i "$GITEA_SSH_KEY" -p "$SSH_PORT" \ + -o ConnectTimeout=3 -o StrictHostKeyChecking=no \ + -o BatchMode=yes "git@${peer_ip}" info 2>/dev/null | grep -q "varaverk\|Gitea\|gitea"; then + GITEA_IP="$peer_ip" + log "Gitea found on Tailscale peer $GITEA_IP" + break + fi + done < <(tailscale status --json 2>/dev/null | \ + python3 -c "import json,sys; d=json.load(sys.stdin); \ + [print(v['TailscaleIPs'][0]) for v in d.get('Peer',{}).values() \ + if v.get('TailscaleIPs')]" 2>/dev/null) fi + + # init-in-place — git clone would fail because the dir already has files. + git -C "$SCRIPTS_DIR" init >> "$LOG" 2>&1 + + CLONED=false + if [[ -n "$GITEA_IP" && -f "$GITEA_SSH_KEY" ]]; then + GITEA_URL="ssh://git@${GITEA_IP}:${SSH_PORT}/${GITEA_REPO_PATH}" + log "trying Gitea: $GITEA_URL" + git -C "$SCRIPTS_DIR" remote add origin "$GITEA_URL" >> "$LOG" 2>&1 + if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT -o StrictHostKeyChecking=no" \ + GIT_TERMINAL_PROMPT=0 \ + git -C "$SCRIPTS_DIR" fetch --depth=1 origin "$BRANCH" >> "$LOG" 2>&1; then + git -C "$SCRIPTS_DIR" reset --hard FETCH_HEAD >> "$LOG" 2>&1 + git -C "$SCRIPTS_DIR" branch -M "$BRANCH" >> "$LOG" 2>&1 + log "scripts installed from Gitea ($GITEA_IP)" + CLONED=true + else + log "Gitea fetch failed — falling back to GitHub" + git -C "$SCRIPTS_DIR" remote remove origin >> "$LOG" 2>&1 || true + fi + fi + + if [[ "$CLONED" == false ]]; then + log "trying GitHub: $GITHUB" + git -C "$SCRIPTS_DIR" remote add origin "$GITHUB" >> "$LOG" 2>&1 + if GIT_TERMINAL_PROMPT=0 \ + git -C "$SCRIPTS_DIR" fetch --depth=1 origin "$BRANCH" >> "$LOG" 2>&1; then + git -C "$SCRIPTS_DIR" reset --hard FETCH_HEAD >> "$LOG" 2>&1 + git -C "$SCRIPTS_DIR" branch -M "$BRANCH" >> "$LOG" 2>&1 + log "scripts installed from GitHub" + CLONED=true + else + log "WARNING: both Gitea and GitHub failed — scripts not installed, retry when network is up" + exit 0 + fi + fi +else + log "repo present — leaving scripts untouched (update from the UI)" fi -log "Install complete — open Varaverk in Unraid to complete setup" -log "→ Plugins → Varaverk (or navigate to Settings → Utilities → Varaverk)" +# Seed master.conf from template if absent. +mkdir -p "$CONF_DIR" +if [[ ! -f "$CONF_DIR/master.conf" && -f "$SCRIPTS_DIR/Deployment/conf_templates/master.conf" ]]; then + cp "$SCRIPTS_DIR/Deployment/conf_templates/master.conf" "$CONF_DIR/master.conf" + log "seeded master.conf from template" +fi +log "install step complete" ]]> - - -/dev/null) -fi -SCRIPTS_DIR="${_sd:-/mnt/user/appdata/Varaverk}" - -# Only act if source exists (array started) and symlink is missing -if [[ -d "$SCRIPTS_DIR/Plugin/unraid" ]] && [[ ! -e "$WEB_DIR" ]]; then - [[ -L "$WEB_DIR" ]] && rm "$WEB_DIR" - ln -s "$SCRIPTS_DIR/Plugin/unraid" "$WEB_DIR" -fi -]]> - - - - - + /dev/null) -fi -SCRIPTS_DIR="${_sd:-/mnt/user/appdata/Varaverk}" +[[ -f "$CFG_FILE" ]] && _sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/dev/null) +SCRIPTS_DIR="${_sd:-$CFG_DIR}" -# ── Stop continuous background scripts ─────────────────────────────────────── -# fallback.sh manages its own lock — use --stop for clean shutdown +# Stop continuous background scripts. if [[ -f "$SCRIPTS_DIR/Fallback/fallback.sh" ]]; then - bash "$SCRIPTS_DIR/Fallback/fallback.sh" --stop 2>/dev/null && \ - log "fallback.sh stopped" || true + bash "$SCRIPTS_DIR/Fallback/fallback.sh" --stop 2>/dev/null && log "fallback.sh stopped" || true fi - -# Kill any remaining Varaverk background processes (watchdog orchestrator, etc.) pkill -f "run_job.sh" 2>/dev/null || true pkill -f "watchdog_orchestrator.sh" 2>/dev/null || true -# ── Remove cron entries ─────────────────────────────────────────────────────── +# Remove cron entries. if [[ -f "$CRON_FILE" ]]; then rm -f "$CRON_FILE" /usr/local/sbin/update_cron 2>/dev/null || true - log "Cron entries removed" + log "cron removed" fi -# Remove legacy direct cron file if it exists rm -f /etc/cron.d/varaverk -# ── Remove web symlink ──────────────────────────────────────────────────────── -[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR" && log "Web symlink removed" +# Remove the installed package (and its RAM files). +removepkg "$PLUGIN" 2>/dev/null || true +[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR" +[[ -d "$WEB_DIR" ]] && rm -rf "$WEB_DIR" +log "web files removed" -# ── Remove plugin config from flash ────────────────────────────────────────── +# Remove flash config (incl. cached .txz). rm -rf "$CFG_DIR" -log "Plugin config removed from flash" - -log "Done. Scripts and conf in $SCRIPTS_DIR are preserved." -log "To fully remove: delete $SCRIPTS_DIR manually." +log "flash config removed" +log "done — scripts/conf in $SCRIPTS_DIR preserved (delete manually for full wipe)" ]]> diff --git a/Rsync/rsync.sh b/Rsync/rsync.sh index d99bc86..ef53754 100644 --- a/Rsync/rsync.sh +++ b/Rsync/rsync.sh @@ -184,7 +184,7 @@ if ! check_rsync_enabled; then fi # Blocklist gate — refuse to sync with a partner blocked after offboard -BLOCKLIST_FILE="/boot/config/partnership_blocklist.db" +BLOCKLIST_FILE="${PARTNERSHIP_BLOCKLIST_FILE:-${STATE_DIR:-/boot/config}/partnership_blocklist.db}" if [[ -f "$BLOCKLIST_FILE" ]] && grep -q "^${REMOTE_SERVER_NAME}|" "$BLOCKLIST_FILE" 2>/dev/null; then error "Rsync blocked — $REMOTE_SERVER_NAME is on the partnership blocklist" error "Re-onboard the partnership to restore access: partnership_manager.sh --onboard" diff --git a/Tools/ramdisk_stop.sh b/Tools/ramdisk_stop.sh index 5412a11..7acca3a 100755 --- a/Tools/ramdisk_stop.sh +++ b/Tools/ramdisk_stop.sh @@ -110,7 +110,7 @@ acquire_lock detect_hosts -STATE_FILE="/tmp/transcode_state.db" +STATE_FILE="${TRANSCODE_STATE_FILE:-${STATE_DIR:-/tmp}/transcode_state.db}" log "Identity: $MY_ID ($LOCAL_SERVER_NAME)" log "Ramdisk: $RAMDISK_PATH" diff --git a/Transcodes/ramdisk_setup.sh b/Transcodes/ramdisk_setup.sh index 607eb97..43aabca 100755 --- a/Transcodes/ramdisk_setup.sh +++ b/Transcodes/ramdisk_setup.sh @@ -327,7 +327,7 @@ fi # ━━━ Initialise State File ━━━ # ============================================================================================== if [[ "$DRY_RUN" == false ]]; then - STATE_FILE="/tmp/transcode_state.db" + STATE_FILE="${TRANSCODE_STATE_FILE:-${STATE_DIR:-/tmp}/transcode_state.db}" NOW=$(date +%s) cat > "$STATE_FILE" </dev/null || true fi +if [[ -n "${STATE_DIR:-}" ]] && [[ ! -d "$STATE_DIR" ]]; then + mkdir -p "$STATE_DIR" 2>/dev/null || true +fi RSYNC_COUNT_FILE="$LOCK_DIR/rsync_active_count" RSYNC_MAX_CONCURRENT=3