Consolidate all paths to plugin flash dir, fix watchdog 7.3 triggers
- Move SCRIPTS_DIR/DATA_DIR/STATE_DIR from appdata to /boot/config/plugins/varaverk - All state files now in STATE_DIR (no more /tmp or /boot/config root writes) - Bootstrap: Gitea-first clone with GitHub fallback, no array dependency - varaverk.cfg seeded with Gitea connection settings - .gitignore: add State_Files/, varaverk.cfg, varaverk-*.txz - Partnership/transcode/fallback scripts use STATE_DIR variables - PHP config.php: DATA_DIR/STATE_DIR constants, VV_SETUP_STATE_FILE dynamic - deploy.sh PROD_ROOT updated to plugin flash dir Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
9191a54637
commit
fb0530deba
+10
@@ -7,9 +7,19 @@ Configurations/*.bak
|
|||||||
|
|
||||||
# ── Runtime state, data, logs ─────────────────────────────────────────────────
|
# ── Runtime state, data, logs ─────────────────────────────────────────────────
|
||||||
data/
|
data/
|
||||||
|
State_Files/
|
||||||
*.log
|
*.log
|
||||||
*.lock
|
*.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 ───────────────────────────────────────────────────────────────
|
# ── OS / editor ───────────────────────────────────────────────────────────────
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
|
|||||||
@@ -111,11 +111,14 @@
|
|||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ── SHARED HOST CONFIGURATION ─────────────────────────────────────────────────────────────────
|
# ── SHARED HOST CONFIGURATION ─────────────────────────────────────────────────────────────────
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# DATA_DIR is the same path on all servers — persistent script state and statistics.
|
# All Varaverk persistent data lives under /mnt/user/appdata/Varaverk/.
|
||||||
# Array share — survives reboots, no flash drive wear.
|
# Both directories are created automatically if they don't exist.
|
||||||
# Created automatically if it doesn't exist.
|
#
|
||||||
# Only truly critical files (fallback state, watchdog reboot log) stay on /boot/config.
|
# DATA_DIR — historical logs, statistics, discovery histories, blocklists
|
||||||
DATA_DIR="/mnt/user/appdata/Varaverk/data"
|
# 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 ──
|
# ── Version Parity ──
|
||||||
# Controls behaviour when local and remote unRAID versions differ.
|
# Controls behaviour when local and remote unRAID versions differ.
|
||||||
@@ -152,9 +155,11 @@
|
|||||||
# All identity vars (hostnames, SSH keys) live in host*.conf.
|
# All identity vars (hostnames, SSH keys) live in host*.conf.
|
||||||
# Hostnames already match Tailscale device names — IP resolution is automatic.
|
# Hostnames already match Tailscale device names — IP resolution is automatic.
|
||||||
#
|
#
|
||||||
# State files on /boot/config — survives reboots, available before array starts:
|
# State files in STATE_DIR — survives reboots, array must be up (scripts run after array start):
|
||||||
# /boot/config/partnership_HOST1.db ← HOST1 writes only
|
# STATE_DIR/partnership_HOST1.db ← HOST1 writes only
|
||||||
# /boot/config/partnership_HOST2.db ← HOST2 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
|
# Propagated via SSH — no rsync needed
|
||||||
#
|
#
|
||||||
# critical_sync_maintenance.sh runs --check every 30min:
|
# critical_sync_maintenance.sh runs --check every 30min:
|
||||||
@@ -185,6 +190,11 @@
|
|||||||
PARTNERSHIP_OFFLINE_THRESHOLD=30 # days either server unreachable before auto-offboard
|
PARTNERSHIP_OFFLINE_THRESHOLD=30 # days either server unreachable before auto-offboard
|
||||||
# works both directions independently
|
# 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.
|
# Tailscale removal on offboard.
|
||||||
PARTNERSHIP_REMOVE_TAILSCALE=true # remove mirror from Tailscale tailnet on offboard
|
PARTNERSHIP_REMOVE_TAILSCALE=true # remove mirror from Tailscale tailnet on offboard
|
||||||
# false = skip removal (manual or testing)
|
# false = skip removal (manual or testing)
|
||||||
@@ -250,7 +260,7 @@
|
|||||||
GITEA_CONTAINER="Gitea"
|
GITEA_CONTAINER="Gitea"
|
||||||
GITEA_REPO_PATH="FailedProxy/Varaverk.git"
|
GITEA_REPO_PATH="FailedProxy/Varaverk.git"
|
||||||
GITEA_DOMAIN="" # e.g. git.yourdomain.com — requires NPM + DNS
|
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"
|
GITEA_SSH_KEY="/root/.ssh/unraid_gitea"
|
||||||
SSH_PORT=221 # Gitea SSH port (default 22, Gitea often uses 221/222)
|
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
|
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_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_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 ━━━
|
# ━━━ Sunday Morning Coffee Report ━━━
|
||||||
# Orchestrator that runs all Sunday monitor scripts in sequence.
|
# Orchestrator that runs all Sunday monitor scripts in sequence.
|
||||||
@@ -633,7 +643,7 @@
|
|||||||
EXTERNAL_IP="8.8.8.8"
|
EXTERNAL_IP="8.8.8.8"
|
||||||
FALLBACK_CHECK_INTERVAL=30 # seconds between fallback state checks
|
FALLBACK_CHECK_INTERVAL=30 # seconds between fallback state checks
|
||||||
FALLBACK_HANDBACK_STRIKES=3 # consecutive healthy checks before initiating handback (3×30s = 90s)
|
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
|
FALLBACK_ENABLED=true # HOST2 back online
|
||||||
# false = suppresses "not running" warnings in status scripts
|
# false = suppresses "not running" warnings in status scripts
|
||||||
FALLBACK_PARTNERSHIP_REQUIRED=true # gate fallback on an active partnership
|
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_SCAN_IGNORE — skip in Tier 2 scan
|
||||||
# HOST*_WATCHDOG_DEPENDENCIES — dependency ordering for restart decisions
|
# HOST*_WATCHDOG_DEPENDENCIES — dependency ordering for restart decisions
|
||||||
|
|
||||||
# Strike state file — /tmp resets on reboot (correct — no stale strikes after reboot)
|
# Strike state file — persistent in STATE_DIR
|
||||||
WATCHDOG_STATE_FILE="/tmp/container_watchdog_state.db"
|
WATCHDOG_STATE_FILE="$STATE_DIR/container_watchdog_state.db"
|
||||||
|
|
||||||
# CPU thresholds — normalised against total core count at runtime
|
# CPU thresholds — normalised against total core count at runtime
|
||||||
SOFT_CPU_THRESHOLD=80 # warn at this % of total system CPU
|
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_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_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_STRIKE_LIMIT=3 # cycles before action fires (matches existing watchdog pattern)
|
||||||
WATCHDOG_APPDATA_GROWTH_FILE="/tmp/watchdog_appdata_growth.db" # /tmp — resets on reboot ✅
|
WATCHDOG_APPDATA_GROWTH_FILE="$STATE_DIR/watchdog_appdata_growth.db"
|
||||||
STORAGE_WATCHDOG_STATE_FILE="/tmp/storage_watchdog_state.db" # /tmp — resets on reboot ✅
|
STORAGE_WATCHDOG_STATE_FILE="$STATE_DIR/storage_watchdog_state.db"
|
||||||
|
|
||||||
# ━━━ Docker Network Connect ━━━
|
# ━━━ Docker Network Connect ━━━
|
||||||
# Ensures custom networks exist and connects containers at array start.
|
# Ensures custom networks exist and connects containers at array start.
|
||||||
@@ -855,7 +865,7 @@
|
|||||||
NETWORK_WATCHDOG_CHECK_TAILSCALE=true
|
NETWORK_WATCHDOG_CHECK_TAILSCALE=true
|
||||||
NETWORK_WATCHDOG_NPM_TIMEOUT=10
|
NETWORK_WATCHDOG_NPM_TIMEOUT=10
|
||||||
NETWORK_WATCHDOG_NPM_STRIKE_LIMIT=2
|
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 ━━━
|
# ━━━ WebGUI Watchdog ━━━
|
||||||
# Monitors unRAID WebGUI responsiveness — escalates through nginx restart → emhttp restart.
|
# Monitors unRAID WebGUI responsiveness — escalates through nginx restart → emhttp restart.
|
||||||
@@ -1164,6 +1174,7 @@
|
|||||||
TRANSCODE_MANAGER_MODE="smart"
|
TRANSCODE_MANAGER_MODE="smart"
|
||||||
|
|
||||||
# Daily statistics log — read by weekly_health_digest.sh for transcode summary.
|
# 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_DAILY_LOG="$DATA_DIR/transcode_daily.db"
|
||||||
TRANSCODE_LOG_RETENTION=90 # days before old entries purged
|
TRANSCODE_LOG_RETENTION=90 # days before old entries purged
|
||||||
|
|
||||||
@@ -1209,7 +1220,7 @@
|
|||||||
|
|
||||||
# ━━━ ZFS Memory Snapshot ━━━
|
# ━━━ ZFS Memory Snapshot ━━━
|
||||||
# Weekly ZFS pool health and memory diagnostic report — informational only.
|
# 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_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_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
|
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)
|
# HOST*_RW_STOP_CONTAINERS — docker stop at hard pressure (in host*.conf)
|
||||||
|
|
||||||
RW_ENABLED=true
|
RW_ENABLED=true
|
||||||
RW_STATE_FILE="/tmp/resource_watchdog_state.db"
|
RW_STATE_FILE="$STATE_DIR/resource_watchdog_state.db"
|
||||||
|
|
||||||
# ━━━ Pressure Thresholds ━━━
|
# ━━━ Pressure Thresholds ━━━
|
||||||
# Graduated RAM response — resource_watchdog acts before system_watchdog reboots.
|
# 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
|
# Warn/shutdown/recover RAM tiers are handled by resource_watchdog.sh
|
||||||
|
|
||||||
# ━━━ State Files ━━━
|
# ━━━ State Files ━━━
|
||||||
SYS_WATCHDOG_STATE_FILE="/tmp/system_watchdog_state.db" # /tmp — resets on reboot ✅
|
SYS_WATCHDOG_STATE_FILE="$STATE_DIR/system_watchdog_state.db"
|
||||||
SYS_WATCHDOG_FAILED_FILE="/boot/config/system_watchdog_failed.db"
|
SYS_WATCHDOG_FAILED_FILE="$STATE_DIR/system_watchdog_failed.db"
|
||||||
SYS_WATCHDOG_REBOOT_LOG="/boot/config/system_watchdog_reboots.db"
|
SYS_WATCHDOG_REBOOT_LOG="$STATE_DIR/system_watchdog_reboots.db"
|
||||||
SYS_WATCHDOG_OOM_FILE="/tmp/system_watchdog_oom.db" # /tmp — resets on reboot ✅
|
SYS_WATCHDOG_OOM_FILE="$STATE_DIR/system_watchdog_oom.db"
|
||||||
|
|
||||||
# ━━━ Strike and Reboot Loop Settings ━━━
|
# ━━━ Strike and Reboot Loop Settings ━━━
|
||||||
# Strike system: a check must fail this many consecutive cycles before action is taken.
|
# Strike system: a check must fail this many consecutive cycles before action is taken.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|
||||||
DEV_ROOT="/mnt/cloud-storage/Important Shit/Git/Development/Varaverk"
|
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"
|
UPGRADE="$PROD_ROOT/Deployment/conf_upgrade.sh"
|
||||||
LOG="/tmp/vv_deploy.log"
|
LOG="/tmp/vv_deploy.log"
|
||||||
|
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ state_init() {
|
|||||||
# Returns 0 (true) if the local partnership DB reports an ACTIVE partnership.
|
# Returns 0 (true) if the local partnership DB reports an ACTIVE partnership.
|
||||||
# Reads /boot/config/partnership_<hostname>.db — no subprocess, no SSH.
|
# Reads /boot/config/partnership_<hostname>.db — no subprocess, no SSH.
|
||||||
check_partnership_active() {
|
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
|
local state
|
||||||
state=$(grep "^state=" "$state_file" 2>/dev/null | cut -d= -f2)
|
state=$(grep "^state=" "$state_file" 2>/dev/null | cut -d= -f2)
|
||||||
[[ "$state" == "ACTIVE" ]]
|
[[ "$state" == "ACTIVE" ]]
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ detect_hosts
|
|||||||
# Defaults — overridden by master.conf values
|
# Defaults — overridden by master.conf values
|
||||||
MONTHLY_UPTIME_THRESHOLD_DAYS="${MONTHLY_UPTIME_THRESHOLD_DAYS:-30}"
|
MONTHLY_UPTIME_THRESHOLD_DAYS="${MONTHLY_UPTIME_THRESHOLD_DAYS:-30}"
|
||||||
MONTHLY_RUN_INTERVAL_DAYS="${MONTHLY_RUN_INTERVAL_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 ))
|
UPTIME_THRESHOLD_SECS=$(( MONTHLY_UPTIME_THRESHOLD_DAYS * 86400 ))
|
||||||
INTERVAL_SECS=$(( MONTHLY_RUN_INTERVAL_DAYS * 86400 ))
|
INTERVAL_SECS=$(( MONTHLY_RUN_INTERVAL_DAYS * 86400 ))
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ DURATION=$(( CYCLE_END - CYCLE_START ))
|
|||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
if [[ "${WATCHDOG_ORCHESTRATOR_HEARTBEAT:-true}" == true ]]; then
|
if [[ "${WATCHDOG_ORCHESTRATOR_HEARTBEAT:-true}" == true ]]; then
|
||||||
HB_SECONDS=$(( ${WATCHDOG_ORCHESTRATOR_HEARTBEAT_HOURS:-1} * 3600 ))
|
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=$(cat "$HB_COUNT_FILE" 2>/dev/null || echo 0)
|
||||||
HB_COUNT=$(( HB_COUNT + 1 ))
|
HB_COUNT=$(( HB_COUNT + 1 ))
|
||||||
echo "$HB_COUNT" > "$HB_COUNT_FILE"
|
echo "$HB_COUNT" > "$HB_COUNT_FILE"
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ OWNER_ID="${PARTNERSHIP_OWNER_HOST:-HOST1}"
|
|||||||
MIRROR_ID=$( [[ "$OWNER_ID" == "HOST1" ]] && echo "HOST2" || echo "HOST1" )
|
MIRROR_ID=$( [[ "$OWNER_ID" == "HOST1" ]] && echo "HOST2" || echo "HOST1" )
|
||||||
MIRROR="${!MIRROR_ID}"
|
MIRROR="${!MIRROR_ID}"
|
||||||
SSH_KEY_PUB="${SSH_KEY}.pub"
|
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"
|
AUTH_KEYS="/root/.ssh/authorized_keys"
|
||||||
MIRROR_SHORT="${MIRROR%%.*}"
|
MIRROR_SHORT="${MIRROR%%.*}"
|
||||||
|
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
source "$SCRIPT_DIR/../load_config.sh"
|
source "$SCRIPT_DIR/../load_config.sh"
|
||||||
|
|
||||||
SSH_TIMEOUT=15
|
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 ────────────────────────────────────────────────────────
|
# ── Parse mode flags before parse_args ────────────────────────────────────────────────────────
|
||||||
MODE=""
|
MODE=""
|
||||||
@@ -267,11 +267,11 @@ AM_MIRROR=false
|
|||||||
[[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true
|
[[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true
|
||||||
|
|
||||||
# State files
|
# State files
|
||||||
LOCAL_STATE_FILE="/boot/config/partnership_${LOCAL_SERVER_NAME}.db"
|
LOCAL_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${LOCAL_SERVER_NAME}.db"
|
||||||
REMOTE_STATE_FILE="/boot/config/partnership_${REMOTE_SERVER_NAME}.db"
|
REMOTE_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${REMOTE_SERVER_NAME}.db"
|
||||||
OWNER_STATE_FILE="/boot/config/partnership_${OWNER}.db"
|
OWNER_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${OWNER}.db"
|
||||||
MIRROR_STATE_FILE="/boot/config/partnership_${MIRROR}.db"
|
MIRROR_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${MIRROR}.db"
|
||||||
OFFLINE_COUNTER="/boot/config/partnership_offline_days.db"
|
OFFLINE_COUNTER="${STATE_DIR:-/boot/config}/partnership_offline_days.db"
|
||||||
|
|
||||||
# ── Exit Trap — restart locally stopped containers if script crashes mid-cleanup ──────────────
|
# ── Exit Trap — restart locally stopped containers if script crashes mid-cleanup ──────────────
|
||||||
# Used by folderview3_remove_partner_folder() and cleanup_partner_containers() — also shared
|
# 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
|
fi
|
||||||
|
|
||||||
# Write HOST1_LOCAL_DONE flag to setup.db
|
# 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
|
if [[ "$DRY_RUN" == false ]]; then
|
||||||
flag_key="${MY_ID}_LOCAL_DONE"
|
flag_key="${MY_ID}_LOCAL_DONE"
|
||||||
if grep -q "^${flag_key}=" "$local_state_file" 2>/dev/null; then
|
if grep -q "^${flag_key}=" "$local_state_file" 2>/dev/null; then
|
||||||
|
|||||||
@@ -117,11 +117,11 @@ AM_MIRROR=false
|
|||||||
[[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true
|
[[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true
|
||||||
[[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true
|
[[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true
|
||||||
|
|
||||||
LOCAL_STATE_FILE="/boot/config/partnership_${LOCAL_SERVER_NAME}.db"
|
LOCAL_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${LOCAL_SERVER_NAME}.db"
|
||||||
REMOTE_STATE_FILE="/boot/config/partnership_${REMOTE_SERVER_NAME}.db"
|
REMOTE_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${REMOTE_SERVER_NAME}.db"
|
||||||
OWNER_STATE_FILE="/boot/config/partnership_${OWNER}.db"
|
OWNER_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${OWNER}.db"
|
||||||
MIRROR_STATE_FILE="/boot/config/partnership_${MIRROR}.db"
|
MIRROR_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${MIRROR}.db"
|
||||||
OFFLINE_COUNTER="/boot/config/partnership_offline_days.db"
|
OFFLINE_COUNTER="${STATE_DIR:-/boot/config}/partnership_offline_days.db"
|
||||||
|
|
||||||
acquire_lock "strict"
|
acquire_lock "strict"
|
||||||
|
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ START=$(date +%s)
|
|||||||
write_onboard_phase() {
|
write_onboard_phase() {
|
||||||
local target_id="$1" phase="$2"
|
local target_id="$1" phase="$2"
|
||||||
local key="${target_id}_PHASE${phase}_DONE"
|
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; }
|
[[ "$DRY_RUN" == true ]] && { warn "DRY RUN — would write ${key}=true"; return 0; }
|
||||||
if grep -q "^${key}=" "$state_file" 2>/dev/null; then
|
if grep -q "^${key}=" "$state_file" 2>/dev/null; then
|
||||||
sed -i "s|^${key}=.*|${key}=true|" "$state_file"
|
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" \
|
OWNER_SCRIPTS_DIR=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \
|
-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:]')
|
'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
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
warn "DRY RUN — would SSH to $OWNER ($OWNER_IP) and trigger Phase 2"
|
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
|
# Write key-ready flag so UI can show the manual-install state
|
||||||
[[ "$DRY_RUN" == false ]] && {
|
[[ "$DRY_RUN" == false ]] && {
|
||||||
local kflag="${MIRROR_ID}_KEY_READY"
|
local kflag="${MIRROR_ID}_KEY_READY"
|
||||||
grep -q "^${kflag}=" /boot/config/varaverk_setup.db 2>/dev/null \
|
local _setup_f="${VARAVERK_SETUP_FILE:-${STATE_DIR:-/boot/config}/varaverk_setup.db}"
|
||||||
&& sed -i "s|^${kflag}=.*|${kflag}=true|" /boot/config/varaverk_setup.db \
|
grep -q "^${kflag}=" "$_setup_f" 2>/dev/null \
|
||||||
|| echo "${kflag}=true" >> /boot/config/varaverk_setup.db
|
&& sed -i "s|^${kflag}=.*|${kflag}=true|" "$_setup_f" \
|
||||||
|
|| echo "${kflag}=true" >> "$_setup_f"
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
STEP_SSH_OK=false
|
STEP_SSH_OK=false
|
||||||
|
|||||||
@@ -145,8 +145,8 @@ AM_MIRROR=false
|
|||||||
[[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true
|
[[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true
|
||||||
[[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true
|
[[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true
|
||||||
|
|
||||||
LOCAL_STATE_FILE="/boot/config/partnership_${LOCAL_SERVER_NAME}.db"
|
LOCAL_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${LOCAL_SERVER_NAME}.db"
|
||||||
REMOTE_STATE_FILE="/boot/config/partnership_${REMOTE_SERVER_NAME}.db"
|
REMOTE_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${REMOTE_SERVER_NAME}.db"
|
||||||
|
|
||||||
acquire_lock "strict"
|
acquire_lock "strict"
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ if [[ "$DRY_RUN" == false ]]; then
|
|||||||
-o ConnectTimeout="$SSH_TIMEOUT" \
|
-o ConnectTimeout="$SSH_TIMEOUT" \
|
||||||
-o StrictHostKeyChecking=no \
|
-o StrictHostKeyChecking=no \
|
||||||
"$SCRIPTS_ROOT/Configurations/master.conf" \
|
"$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 ✅" || \
|
log "master.conf pushed to $NEW_OWNER ✅" || \
|
||||||
error "Failed to push master.conf to $NEW_OWNER — set PARTNERSHIP_OWNER_HOST=\"$NEW_OWNER_ID\" manually"
|
error "Failed to push master.conf to $NEW_OWNER — set PARTNERSHIP_OWNER_HOST=\"$NEW_OWNER_ID\" manually"
|
||||||
else
|
else
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -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'
|
||||||
|
<?xml version='1.0' standalone='yes'?>
|
||||||
|
<!DOCTYPE PLUGIN [
|
||||||
|
<!ENTITY name "varaverk">
|
||||||
|
<!ENTITY author "gmer4lfe">
|
||||||
|
<!ENTITY version "2026.05.28">
|
||||||
|
]>
|
||||||
|
<PLUGIN name="&name;" author="&author;" version="&version;" launch="varaverk/monitor" icon="/plugins/varaverk/icons/varaverk.png">
|
||||||
|
</PLUGIN>
|
||||||
|
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
|
||||||
|
<!ENTITY version "2026.05.28">
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ━━━ ADDING A NEW OS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
|
`dev_install.sh` is built to support multiple OS targets. To add one:
|
||||||
|
|
||||||
|
1. Create `Plugin/<os>/` with the OS-appropriate web app files
|
||||||
|
2. Add the OS marker to `detect_os()` in `dev_install.sh`:
|
||||||
|
```bash
|
||||||
|
elif [[ -f /etc/<os>-marker ]]; then echo "<os>"
|
||||||
|
```
|
||||||
|
3. Add the install target to the `case` block:
|
||||||
|
```bash
|
||||||
|
<os>)
|
||||||
|
SOURCE="$SCRIPT_DIR/<os>"
|
||||||
|
TARGET="/path/to/web/server/plugins/$PLUGIN_NAME"
|
||||||
|
;;
|
||||||
|
```
|
||||||
@@ -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) |
|
||||||
Executable
+108
@@ -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-<version>-noarch-1.txz the package unRAID installs to
|
||||||
|
# /usr/local/emhttp/plugins/varaverk/
|
||||||
|
# varaverk-<version>-noarch-1.txz.sha256
|
||||||
|
#
|
||||||
|
# It also rewrites the <!ENTITY version> and <!ENTITY sha256> 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/<txz> + the updated .plg, then attach the
|
||||||
|
# .txz to a GitHub release tagged <version> 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|(<!ENTITY[[:space:]]+version[[:space:]]+\")[^\"]*(\">)|\1${VERSION}\2|" "$PLG"
|
||||||
|
sed -i -E "s|(<!ENTITY[[:space:]]+sha256[[:space:]]+\")[^\"]*(\">)|\1${SHA256}\2|" "$PLG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SIZE="$(numfmt --to=iec "$(stat -c %s "$OUTFILE")")"
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Built: $OUTFILE ($SIZE)
|
||||||
|
Version: $VERSION
|
||||||
|
SHA256: $SHA256
|
||||||
|
|
||||||
|
.plg updated (version + sha256).
|
||||||
|
|
||||||
|
Next:
|
||||||
|
1) git add Plugin/dist/${PKG_BASENAME}.txz* Plugin/varaverk.plg && commit
|
||||||
|
2) Create GitHub release tagged "$VERSION", attach ${PKG_BASENAME}.txz
|
||||||
|
(skip for a local/offline install — copy the .txz to
|
||||||
|
/boot/config/plugins/$PLUGIN_NAME/ and the .plg uses the cached copy)
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
// Diagnostic endpoint — tests the Unraid GraphQL API and returns raw results.
|
||||||
|
// Hit from browser: /plugins/varaverk/api/api_test.php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
|
||||||
|
$hostId = vv_detect_host();
|
||||||
|
$vars = vv_conf_vars();
|
||||||
|
$key = $vars[strtoupper($hostId) . '_UNRAID_API_KEY'] ?? '';
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'host' => $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);
|
||||||
@@ -36,7 +36,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$results = vv_conf_write_changes($changes);
|
$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;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Cache-Control: no-store, no-cache');
|
||||||
|
require_once dirname(__DIR__) . '/include/config.php';
|
||||||
|
|
||||||
|
$host = vv_detect_host();
|
||||||
|
if (!preg_match('/^host\d+$/', $host)) {
|
||||||
|
echo json_encode(['ok' => 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 </dev/null 2>&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,
|
||||||
|
]);
|
||||||
@@ -11,4 +11,12 @@ if (!$name || !preg_match('/^[A-Z_]+_RSYNC_ENABLED$/', $name)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$ok = vv_conf_flag_set($name, $enabled);
|
$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]);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
// Connectivity test — SSH echo to a partner host with round-trip latency.
|
||||||
|
// GET ?id=HOST2 (GET avoids the bodyless-POST issue on this nginx setup).
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Cache-Control: no-store, no-cache');
|
||||||
|
require_once dirname(__DIR__) . '/include/partnership.php';
|
||||||
|
|
||||||
|
$id = trim($_GET['id'] ?? '');
|
||||||
|
if (!preg_match('/^host\d+$/i', $id)) {
|
||||||
|
echo json_encode(['ok' => false, 'error' => 'Invalid host id']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(vv_pt_ping($id));
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
// Partnership-related settings for this host — the Partnership section of each accessible conf.
|
||||||
|
// HOST1 (owner): master.conf PARTNERSHIP + host1.conf Partnership. Other hosts: their own.
|
||||||
|
// Uses vv_conf_all_groups() (handles master.conf's sandwiched major header) then filters
|
||||||
|
// to partnership sections. Writes go through confform.php (which pushes master.conf to partners).
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Cache-Control: no-store, no-cache');
|
||||||
|
require_once dirname(__DIR__) . '/include/confform.php';
|
||||||
|
|
||||||
|
$files = vv_get_conf_files();
|
||||||
|
$out = [];
|
||||||
|
foreach ($files as $f) {
|
||||||
|
$groups = array_values(array_filter(
|
||||||
|
vv_conf_all_groups($f),
|
||||||
|
fn($g) => stripos($g['subsection'], 'partnership') !== false
|
||||||
|
));
|
||||||
|
if ($groups) {
|
||||||
|
$out[] = ['file' => $f, 'groups' => $groups];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['ok' => true, 'files' => $out]);
|
||||||
@@ -50,7 +50,7 @@ if ($action === 'pull') {
|
|||||||
. ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip;
|
. ' -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"') ?: '');
|
$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);
|
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
|
// SCP master.conf from HOST1
|
||||||
$localMaster = CONF_DIR . '/master.conf';
|
$localMaster = CONF_DIR . '/master.conf';
|
||||||
|
|||||||
@@ -584,7 +584,17 @@ function vv_remote_hosts_stats(): array {
|
|||||||
if ($cached) { $results[$id] = $cached; continue; }
|
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);
|
$data = vv_unraid_api_query(strtolower($id), $gql, 4, $key);
|
||||||
|
|
||||||
if (!$data) {
|
if (!$data) {
|
||||||
@@ -594,22 +604,17 @@ function vv_remote_hosts_stats(): array {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$os = $data['info']['os'] ?? [];
|
$os = $data['info']['os'] ?? [];
|
||||||
$cpu = $data['info']['cpu'] ?? [];
|
$cpu = $data['info']['cpu'] ?? [];
|
||||||
$metrics = $data['metrics'] ?? [];
|
$mMem = $data['metrics']['memory'] ?? [];
|
||||||
$mCpu = $metrics['cpu'] ?? [];
|
|
||||||
$mMem = $metrics['memory'] ?? [];
|
|
||||||
$arr = $data['array'] ?? [];
|
|
||||||
|
|
||||||
$cpuLoad = round((float)($mCpu['percentTotal'] ?? 0), 1);
|
$memPct = round((float)($mMem['percentTotal'] ?? 0));
|
||||||
$memPct = round((float)($mMem['percentTotal'] ?? 0));
|
|
||||||
// Also compute from raw bytes as cross-check when percentTotal is missing
|
|
||||||
if ($memPct === 0) {
|
if ($memPct === 0) {
|
||||||
$totalBytes = (float)($mMem['total'] ?? 0);
|
$totalBytes = (float)($mMem['total'] ?? 0);
|
||||||
$availBytes = (float)($mMem['available'] ?? 0);
|
$availBytes = (float)($mMem['available'] ?? 0);
|
||||||
$memPct = $totalBytes > 0 ? (int)round(($totalBytes - $availBytes) / $totalBytes * 100) : 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'] ?? '';
|
$uptimeRaw = $os['uptime'] ?? '';
|
||||||
if (is_numeric($uptimeRaw)) {
|
if (is_numeric($uptimeRaw)) {
|
||||||
@@ -623,19 +628,20 @@ function vv_remote_hosts_stats(): array {
|
|||||||
$uptime = $uptimeRaw ?: '—';
|
$uptime = $uptimeRaw ?: '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
$entry = [
|
$nodeMetrics = vv_api_node_metrics($data);
|
||||||
|
$entry = array_merge([
|
||||||
'available' => true,
|
'available' => true,
|
||||||
'host_id' => $id,
|
'host_id' => $id,
|
||||||
'hostname' => $os['hostname'] ?? $vars[$id],
|
'hostname' => $os['hostname'] ?? $vars[$id],
|
||||||
'version' => $os['release'] ?? '',
|
'version' => $os['release'] ?? '',
|
||||||
'uptime' => $uptime,
|
'uptime' => $uptime,
|
||||||
'uptime_sec' => $uptimeSec,
|
'uptime_sec' => $uptimeSec,
|
||||||
'cpu_load' => $cpuLoad,
|
'cpu_load' => $nodeMetrics['cpu_pct'] ?? 0,
|
||||||
'cpu_threads' => (int)($cpu['threads'] ?? 0),
|
'cpu_threads' => (int)($cpu['threads'] ?? 0),
|
||||||
'mem_total_gb' => $memTotalGb,
|
'mem_total_gb' => $memTotalGb,
|
||||||
'mem_used_pct' => $memPct,
|
'mem_used_pct' => $memPct,
|
||||||
'array_state' => $arr['state'] ?? 'UNKNOWN',
|
'array_state' => $data['array']['state'] ?? 'UNKNOWN',
|
||||||
];
|
], $nodeMetrics);
|
||||||
file_put_contents($cacheFile, json_encode($entry));
|
file_put_contents($cacheFile, json_encode($entry));
|
||||||
$results[$id] = $entry;
|
$results[$id] = $entry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
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 = [];
|
$fields = [];
|
||||||
$pendingDesc = [];
|
$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.
|
// Return all conf groups (subsection + fields) for a script on the current host.
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
define('PLUGIN_CFG', '/boot/config/plugins/varaverk/varaverk.cfg');
|
define('PLUGIN_CFG', '/boot/config/plugins/varaverk/varaverk.cfg');
|
||||||
|
|
||||||
$_vv_cfg = @parse_ini_file(PLUGIN_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('CONF_DIR', SCRIPTS_DIR . '/Configurations');
|
||||||
|
define('DATA_DIR', SCRIPTS_DIR . '/data');
|
||||||
|
define('STATE_DIR', SCRIPTS_DIR . '/State_Files');
|
||||||
define('LOG_DIR', '/var/log/varaverk');
|
define('LOG_DIR', '/var/log/varaverk');
|
||||||
unset($_vv_cfg);
|
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.
|
// Read the setup state file into a key=>value array.
|
||||||
function vv_setup_state_read(): array {
|
function vv_setup_state_read(): array {
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ function vv_watchdog_summary(): array {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Recent restarts (24 h)
|
// 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) ?: '';
|
$restartRaw = @file_get_contents($restartLog) ?: '';
|
||||||
$cutoff = time() - 86400;
|
$cutoff = time() - 86400;
|
||||||
$restarts = [];
|
$restarts = [];
|
||||||
@@ -297,6 +297,7 @@ function vv_scripts_status(): array {
|
|||||||
$ts = (int)($stat['end'] ?? $stat['start'] ?? @filemtime($statFile) ?: 0);
|
$ts = (int)($stat['end'] ?? $stat['start'] ?? @filemtime($statFile) ?: 0);
|
||||||
|
|
||||||
$scripts[] = [
|
$scripts[] = [
|
||||||
|
'id' => $id,
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
'last_ts' => $ts,
|
'last_ts' => $ts,
|
||||||
'status' => $status,
|
'status' => $status,
|
||||||
|
|||||||
@@ -3,23 +3,63 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/config.php';
|
require_once __DIR__ . '/config.php';
|
||||||
require_once __DIR__ . '/arrs.php'; // vv_arr_known_hosts(), vv_arr_scalar()
|
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 ────────────────────────────────────────────────────────────────────
|
// ── Config ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function vv_pt_config(): array {
|
function vv_pt_config(): array {
|
||||||
$v = vv_conf_vars();
|
$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 [
|
return [
|
||||||
'enabled' => ($v['PARTNERSHIP_ENABLED'] ?? 'false') === 'true',
|
'enabled' => ($v['PARTNERSHIP_ENABLED'] ?? 'false') === 'true',
|
||||||
'owner_host' => $v['PARTNERSHIP_OWNER_HOST'] ?? '',
|
'owner_host' => $v['PARTNERSHIP_OWNER_HOST'] ?? '',
|
||||||
'sync_min' => (int)($v['PARTNERSHIP_SYNC_INTERVAL'] ?? 15),
|
'sync_min' => (int)($v['PARTNERSHIP_SYNC_INTERVAL'] ?? 15),
|
||||||
'grace_hours' => (int)($v['PARTNERSHIP_GRACE_HOURS'] ?? 6),
|
'grace_hours' => (int)($v['PARTNERSHIP_GRACE_HOURS'] ?? 6),
|
||||||
'offline_threshold' => (int)($v['PARTNERSHIP_OFFLINE_THRESHOLD'] ?? 30),
|
'offline_threshold' => (int)($v['PARTNERSHIP_OFFLINE_THRESHOLD'] ?? 30),
|
||||||
|
'offline_days' => $offlineDays,
|
||||||
'remove_tailscale' => ($v['PARTNERSHIP_REMOVE_TAILSCALE'] ?? 'true') === 'true',
|
'remove_tailscale' => ($v['PARTNERSHIP_REMOVE_TAILSCALE'] ?? 'true') === 'true',
|
||||||
'folderview3' => ($v['PARTNERSHIP_FOLDERVIEW3'] ?? 'false') === 'true',
|
'folderview3' => ($v['PARTNERSHIP_FOLDERVIEW3'] ?? 'false') === 'true',
|
||||||
'tailscale_configured' => !empty($v['TAILSCALE_API_KEY']) && !empty($v['TAILSCALE_TAILNET']),
|
'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 ─────────────────────────────────────────────────────────
|
// ── State file parser ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function vv_pt_read_db(string $path): array {
|
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 ───────────────────────────────────────────────────────────────
|
// ── 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 {
|
function vv_pt_local_system(): array {
|
||||||
$ver = '';
|
$info = vv_system_info();
|
||||||
if (file_exists('/etc/unraid-version')) {
|
$uptimeSec = file_exists('/proc/uptime')
|
||||||
preg_match('/VERSION="([^"]+)"/', file_get_contents('/etc/unraid-version'), $m);
|
? (int)explode(' ', file_get_contents('/proc/uptime'))[0] : 0;
|
||||||
$ver = $m[1] ?? '';
|
return [
|
||||||
}
|
'unraid_version' => $info['version'] ?? '',
|
||||||
$uptime = 0;
|
'uptime_sec' => $uptimeSec,
|
||||||
if (file_exists('/proc/uptime')) {
|
'load_avg' => isset($info['load_avg']) ? $info['load_avg'][0] : null,
|
||||||
$uptime = (int)explode(' ', file_get_contents('/proc/uptime'))[0];
|
'containers' => count(vv_docker_containers()),
|
||||||
}
|
];
|
||||||
return ['unraid_version' => $ver, 'uptime_sec' => $uptime];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
function vv_pt_remote_system(string $ip, string $sshKey): array {
|
||||||
$out = vv_pt_ssh($ip, $sshKey,
|
$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 = '';
|
$ver = '';
|
||||||
preg_match('/VERSION="([^"]+)"/', $out, $m);
|
preg_match('/VERSION="([^"]+)"/', $out, $m); if ($m) $ver = $m[1];
|
||||||
if ($m) $ver = $m[1];
|
|
||||||
$uptime = 0;
|
$uptime = 0;
|
||||||
if (preg_match('/UPTIME:([\d.]+)/', $out, $m)) $uptime = (int)$m[1];
|
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 ─────────────────────────────────────────────────────────────
|
// ── Per-node data ─────────────────────────────────────────────────────────────
|
||||||
@@ -110,6 +166,9 @@ function vv_pt_nodes(): array {
|
|||||||
$ownerSlot = strtolower($vars['PARTNERSHIP_OWNER_HOST'] ?? '');
|
$ownerSlot = strtolower($vars['PARTNERSHIP_OWNER_HOST'] ?? '');
|
||||||
$setupDb = vv_setup_state_read();
|
$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
|
// SSH key for this host
|
||||||
$myId = strtoupper($currentHost);
|
$myId = strtoupper($currentHost);
|
||||||
$myRaw = vv_read_conf_raw($currentHost . '.conf');
|
$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)
|
// For self: local setup complete flag (set by partnership_manager --onboard --local-only)
|
||||||
$localDone = $isMe && ($setupDb[$nodeIdUpper . '_LOCAL_DONE'] ?? '') === 'true';
|
$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 2>/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[] = [
|
$nodes[] = [
|
||||||
'slot' => $slot,
|
'slot' => $slot,
|
||||||
'id' => $nodeIdUpper,
|
'id' => $nodeIdUpper,
|
||||||
'hostname' => $hostname,
|
'hostname' => $hostname,
|
||||||
'is_me' => $isMe,
|
'is_me' => $isMe,
|
||||||
'is_owner' => $isOwner,
|
'is_owner' => $isOwner,
|
||||||
'ts_online' => $ts['online'],
|
'ts_online' => $ts['online'],
|
||||||
'ts_active' => $ts['active'],
|
'ts_active' => $ts['active'],
|
||||||
'ts_ip' => $ts['ip'],
|
'ts_ip' => $ts['ip'],
|
||||||
'fallback' => $fbState,
|
'fallback' => $fbState,
|
||||||
'partnership' => $ptDb,
|
'partnership' => $ptDb,
|
||||||
'system' => $system,
|
'system' => $system,
|
||||||
'onboard_phase' => $onboardPhase,
|
'onboard_phase' => $onboardPhase,
|
||||||
'key_ready' => $keyReady,
|
'key_ready' => $keyReady,
|
||||||
'local_done' => $localDone,
|
'local_done' => $localDone,
|
||||||
|
'api_key_set' => $apiKeySet,
|
||||||
|
'api_key_preview' => $apiKeyPreview,
|
||||||
|
'metrics' => $metrics,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return $nodes;
|
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 ───────────────────────────────────────────────────────────────
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function vv_partnership_all(): array {
|
function vv_partnership_all(): array {
|
||||||
return [
|
return [
|
||||||
'config' => vv_pt_config(),
|
'config' => vv_pt_config(),
|
||||||
'nodes' => vv_pt_nodes(),
|
'nodes' => vv_pt_nodes(),
|
||||||
|
'sync' => vv_pt_sync(),
|
||||||
'ts' => time(),
|
'ts' => time(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) ─────────────────
|
// ── 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
|
// 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.
|
// to hostn.conf, then run Deployment/deploy.sh. No schema work needed.
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ function vv_wd_all(): array {
|
|||||||
$currentHost = vv_detect_host();
|
$currentHost = vv_detect_host();
|
||||||
$tsPeers = vv_pt_ts_peers();
|
$tsPeers = vv_pt_ts_peers();
|
||||||
$masterRaw = vv_read_conf_raw('master.conf');
|
$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
|
// Config thresholds from master.conf
|
||||||
$cfg = [
|
$cfg = [
|
||||||
|
|||||||
@@ -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 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 name = s.name.length > 24 ? s.name.slice(0, 23) + '…' : s.name;
|
||||||
const dur = s.duration != null ? ` ${s.duration}s` : '';
|
const dur = s.duration != null ? ` ${s.duration}s` : '';
|
||||||
listHtml += `<div style="display:flex;align-items:center;gap:5px;margin-bottom:4px;font-size:11px;">
|
const sid = (s.id || '').replace(/\\/g,'\\\\').replace(/'/g,"\\'");
|
||||||
|
listHtml += `<div onclick="vvOpenScript('${sid}')" title="Open ${name} in Scheduler"
|
||||||
|
style="display:flex;align-items:center;gap:5px;margin-bottom:4px;font-size:11px;cursor:pointer;"
|
||||||
|
onmouseover="this.style.background='#161616'" onmouseout="this.style.background=''">
|
||||||
<span style="color:${color};flex-shrink:0;width:10px;text-align:center;">${icon}</span>
|
<span style="color:${color};flex-shrink:0;width:10px;text-align:center;">${icon}</span>
|
||||||
<span style="color:#888;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${name}</span>
|
<span style="color:#888;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${name}</span>
|
||||||
<span style="color:#444;font-size:10px;flex-shrink:0;">${ago}${dur}</span>
|
<span style="color:#444;font-size:10px;flex-shrink:0;">${ago}${dur}</span>
|
||||||
@@ -485,6 +488,12 @@ function vvScriptsFilterSet(type) {
|
|||||||
vvRenderScripts();
|
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) ─────
|
// ── Disk / storage helpers (module-level so vvRenderPools can call them) ─────
|
||||||
|
|
||||||
function vvTempColor(tempC, transport) {
|
function vvTempColor(tempC, transport) {
|
||||||
@@ -575,6 +584,16 @@ function vvPollMonitor() {
|
|||||||
const _runningVMs = (d.vms?.vms ?? []).filter(v => v.state === 'running').length;
|
const _runningVMs = (d.vms?.vms ?? []).filter(v => v.state === 'running').length;
|
||||||
const _threadInfo = sys.cpu_threads ? `${sys.cpu_cores}c / ${sys.cpu_threads}t` : '';
|
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)} <span style="color:#444;">·</span> ${_load[1].toFixed(2)} <span style="color:#444;">·</span> ${_load[2].toFixed(2)}`
|
||||||
|
: '—';
|
||||||
|
const _coreMeta = _threadInfo ? ` <span style="color:#444;">(${_threadInfo})</span>` : '';
|
||||||
|
|
||||||
document.getElementById('vv-system-body').innerHTML =
|
document.getElementById('vv-system-body').innerHTML =
|
||||||
`<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;">
|
`<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px;">
|
||||||
<div style="min-width:0;">
|
<div style="min-width:0;">
|
||||||
@@ -608,9 +627,10 @@ function vvPollMonitor() {
|
|||||||
<div style="font-size:20px;font-weight:300;color:#ccc;line-height:1;">${timeStr}</div>
|
<div style="font-size:20px;font-weight:300;color:#ccc;line-height:1;">${timeStr}</div>
|
||||||
<div style="font-size:10px;color:#555;margin-bottom:10px;">${dateStr} · ${tz}</div>
|
<div style="font-size:10px;color:#555;margin-bottom:10px;">${dateStr} · ${tz}</div>
|
||||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:3px 8px;font-size:11px;">
|
<div style="display:grid;grid-template-columns:auto 1fr;gap:3px 8px;font-size:11px;">
|
||||||
<span style="color:#444;">Model</span> <span style="color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${sys.cpu_model}${_threadInfo ? ` <span style="color:#444;">(${_threadInfo})</span>` : ''}</span>
|
<span style="color:#444;">Model</span> <span style="color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${sys.cpu_model}${_coreMeta}</span>
|
||||||
<span style="color:#444;">Array</span> <span style="color:${arrayColor};font-weight:600;">${sys.array_state}</span>
|
<span style="color:#444;">Array</span> <span style="color:${arrayColor};font-weight:600;">${sys.array_state}</span>
|
||||||
<span style="color:#444;">Uptime</span> <span style="color:#888;">${sys.uptime}</span>
|
<span style="color:#444;">Uptime</span> <span style="color:#888;">${sys.uptime}</span>
|
||||||
|
<span style="color:#444;">Load</span> <span style="color:${_loadColor};">${_loadStr}</span>
|
||||||
<span style="color:#444;">Running</span> <span style="color:#888;">${_runningCtrs} ctr${_runningCtrs !== 1 ? 's' : ''}${_runningVMs > 0 ? ` · ${_runningVMs} VM` : ''}</span>
|
<span style="color:#444;">Running</span> <span style="color:#888;">${_runningCtrs} ctr${_runningCtrs !== 1 ? 's' : ''}${_runningVMs > 0 ? ` · ${_runningVMs} VM` : ''}</span>
|
||||||
<span style="color:#444;">Version</span> <span style="color:#3a3a3a;">${ver}</span>
|
<span style="color:#444;">Version</span> <span style="color:#3a3a3a;">${ver}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -856,6 +876,8 @@ function vvPollMonitor() {
|
|||||||
|
|
||||||
const maxSeen = Math.max(...vvNetRxHistory, ...vvNetTxHistory, 1);
|
const maxSeen = Math.max(...vvNetRxHistory, ...vvNetTxHistory, 1);
|
||||||
const maxBps = maxSeen * 1.25; // auto-scale with 25% headroom
|
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 = [
|
const ipRows = [
|
||||||
net.local_ip ? `<div><span style="color:#555;font-size:9px;">LAN </span>${net.local_ip}</div>` : '',
|
net.local_ip ? `<div><span style="color:#555;font-size:9px;">LAN </span>${net.local_ip}</div>` : '',
|
||||||
@@ -868,8 +890,8 @@ function vvPollMonitor() {
|
|||||||
<div>
|
<div>
|
||||||
<div style="font-size:12px;color:#888;margin-bottom:4px;">${net.iface} · ${linkLabel}</div>
|
<div style="font-size:12px;color:#888;margin-bottom:4px;">${net.iface} · ${linkLabel}</div>
|
||||||
<div style="display:flex;gap:16px;font-size:13px;font-weight:600;">
|
<div style="display:flex;gap:16px;font-size:13px;font-weight:600;">
|
||||||
<span><span style="color:#4caf50;font-size:9px;margin-right:4px;">━ IN (RX)</span><span style="color:#4caf50;">${vvFmtBps(rx)}</span></span>
|
<span><span style="color:#4caf50;font-size:9px;margin-right:4px;">━ IN (RX)</span><span style="color:#4caf50;">${vvFmtBps(rx)}</span><span style="color:#444;font-size:9px;font-weight:400;margin-left:4px;">peak ${vvFmtBps(peakRx)}</span></span>
|
||||||
<span><span style="color:#ff9800;font-size:9px;margin-right:4px;">━ OUT (TX)</span><span style="color:#ff9800;">${vvFmtBps(tx)}</span></span>
|
<span><span style="color:#ff9800;font-size:9px;margin-right:4px;">━ OUT (TX)</span><span style="color:#ff9800;">${vvFmtBps(tx)}</span><span style="color:#444;font-size:9px;font-weight:400;margin-left:4px;">peak ${vvFmtBps(peakTx)}</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align:right;font-size:11px;color:#aaa;line-height:1.6;">${ipRows}</div>
|
<div style="text-align:right;font-size:11px;color:#aaa;line-height:1.6;">${ipRows}</div>
|
||||||
@@ -881,9 +903,15 @@ function vvPollMonitor() {
|
|||||||
document.getElementById('vv-network-body').innerHTML = '<p style="color:#555;font-style:italic">No network interface detected</p>';
|
document.getElementById('vv-network-body').innerHTML = '<p style="color:#555;font-style:italic">No network interface detected</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CPU title ────────────────────────────────────────────────────────────
|
// ── CPU title (core count + temp to its right) ────────────────────────────
|
||||||
const _cpuTitleEl = document.getElementById('vv-cpu-title');
|
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 ? ` <span style="color:${_tColor};font-weight:400;">${_cpuTemp}°</span>` : '');
|
||||||
|
}
|
||||||
|
|
||||||
// ── UPS / Power ─────────────────────────────────────────────────────────
|
// ── UPS / Power ─────────────────────────────────────────────────────────
|
||||||
const ups = d.ups ?? {};
|
const ups = d.ups ?? {};
|
||||||
@@ -1072,13 +1100,15 @@ function vvPollMonitor() {
|
|||||||
const sshdOk = stab.sshd_ok ?? true;
|
const sshdOk = stab.sshd_ok ?? true;
|
||||||
const zombies = stab.zombies ?? 0;
|
const zombies = stab.zombies ?? 0;
|
||||||
|
|
||||||
let statsHtml = `<div style="display:grid;grid-template-columns:auto 1fr;gap:2px 8px;font-size:11px;margin-top:8px;margin-bottom:6px;">
|
const cpuRow = stab.cpu_temp != null
|
||||||
|
? `<span style="color:#444;">CPU</span><span style="color:${wdPct(stab.cpu_temp,75,90)};">${stab.cpu_temp}°C</span>` : '';
|
||||||
|
let statsHtml = `<div style="display:grid;grid-template-columns:auto 1fr auto 1fr;gap:2px 8px;font-size:11px;margin-top:8px;margin-bottom:6px;">
|
||||||
<span style="color:#444;">rootfs</span><span style="color:${wdPct(stab.rootfs_pct??0,75,90)};">${stab.rootfs_pct??0}%</span>
|
<span style="color:#444;">rootfs</span><span style="color:${wdPct(stab.rootfs_pct??0,75,90)};">${stab.rootfs_pct??0}%</span>
|
||||||
<span style="color:#444;">/var/log</span><span style="color:${wdPct(stab.log_pct??0,75,90)};">${stab.log_pct??0}%</span>
|
<span style="color:#444;">/var/log</span><span style="color:${wdPct(stab.log_pct??0,75,90)};">${stab.log_pct??0}%</span>
|
||||||
<span style="color:#444;">/tmp</span><span style="color:${wdPct(stab.tmp_pct??0,75,90)};">${stab.tmp_pct??0}%</span>
|
<span style="color:#444;">/tmp</span><span style="color:${wdPct(stab.tmp_pct??0,75,90)};">${stab.tmp_pct??0}%</span>
|
||||||
<span style="color:#444;">RAM free</span><span style="color:${ramColor};">${ramFree}GB</span>
|
<span style="color:#444;">RAM free</span><span style="color:${ramColor};">${ramFree}GB</span>
|
||||||
<span style="color:#444;">Load</span><span style="color:${loadColor};">${load}</span>
|
<span style="color:#444;">Load</span><span style="color:${loadColor};">${load}</span>
|
||||||
${stab.cpu_temp != null ? `<span style="color:#444;">CPU</span><span style="color:${wdPct(stab.cpu_temp,75,90)};">${stab.cpu_temp}°C</span>` : ''}
|
${cpuRow}
|
||||||
<span style="color:#444;">Zombies</span><span style="color:${zombies>0?'#ff9800':'#4caf50'};">${zombies}</span>
|
<span style="color:#444;">Zombies</span><span style="color:${zombies>0?'#ff9800':'#4caf50'};">${zombies}</span>
|
||||||
<span style="color:#444;">${stab.nic??'nic'}</span><span style="color:${nicOk?'#4caf50':'#f44336'};">● ${stab.nic_state??'?'}</span>
|
<span style="color:#444;">${stab.nic??'nic'}</span><span style="color:${nicOk?'#4caf50':'#f44336'};">● ${stab.nic_state??'?'}</span>
|
||||||
<span style="color:#444;">sshd</span><span style="color:${sshdOk?'#4caf50':'#f44336'};">${sshdOk?'● ok':'✗ down'}</span>
|
<span style="color:#444;">sshd</span><span style="color:${sshdOk?'#4caf50':'#f44336'};">${sshdOk?'● ok':'✗ down'}</span>
|
||||||
@@ -1208,6 +1238,22 @@ function vvPollMonitor() {
|
|||||||
const procCount = gpuProcs.length;
|
const procCount = gpuProcs.length;
|
||||||
const procColor = procCount > 0 ? '#4caf50' : '#555';
|
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 `<div style="display:flex;justify-content:space-between;font-size:10px;margin-bottom:2px;">
|
||||||
|
<span style="color:#888;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;">${pname}</span>
|
||||||
|
<span style="color:#555;flex-shrink:0;margin-left:6px;">${p.memory_mb} MB</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
gpuProcHtml = `<div style="margin-top:6px;border-top:1px solid #2a2a2a;padding-top:6px;">
|
||||||
|
<div style="font-size:10px;color:#555;margin-bottom:3px;">GPU processes</div>
|
||||||
|
<div style="max-height:60px;overflow-y:auto;">${rows}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('vv-gpu-body').innerHTML =
|
document.getElementById('vv-gpu-body').innerHTML =
|
||||||
// header row: name + process count pill
|
// header row: name + process count pill
|
||||||
`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
`<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
|
||||||
@@ -1224,7 +1270,7 @@ function vvPollMonitor() {
|
|||||||
<span style="color:${tempColor};font-weight:bold;">${temp}°C</span>
|
<span style="color:${tempColor};font-weight:bold;">${temp}°C</span>
|
||||||
<span style="color:#666;margin-left:12px;">Power</span>
|
<span style="color:#666;margin-left:12px;">Power</span>
|
||||||
<span style="color:#aaa;font-weight:bold;">${powerStr}</span>
|
<span style="color:#aaa;font-weight:bold;">${powerStr}</span>
|
||||||
</div>`;
|
</div>` + gpuProcHtml;
|
||||||
} else {
|
} else {
|
||||||
document.getElementById('vv-gpu-body').innerHTML = '<p style="color:#555;font-style:italic">No GPU detected</p>';
|
document.getElementById('vv-gpu-body').innerHTML = '<p style="color:#555;font-style:italic">No GPU detected</p>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,28 @@
|
|||||||
0%,100% { box-shadow: 0 0 0 2px #4a8, 0 0 8px rgba(100,200,120,.2); }
|
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); }
|
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; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 2px;">
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;padding:0 2px;">
|
||||||
@@ -44,17 +66,46 @@
|
|||||||
<div id="vv-pt-config-body" style="color:#444;font-size:12px;">Loading…</div>
|
<div id="vv-pt-config-body" style="color:#444;font-size:12px;">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Offline countdown warning (shown only when partner has been unreachable) -->
|
||||||
|
<div id="vv-pt-offline-warn" style="display:none;margin-bottom:12px;"></div>
|
||||||
|
|
||||||
<!-- Node grid -->
|
<!-- Node grid -->
|
||||||
<div id="vv-pt-nodes" class="vv-pt-grid" style="margin-bottom:12px;">
|
<div id="vv-pt-nodes" class="vv-pt-grid" style="margin-bottom:12px;">
|
||||||
<div style="color:#444;font-size:12px;padding:16px 0;text-align:center;">Loading…</div>
|
<div style="color:#444;font-size:12px;padding:16px 0;text-align:center;">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mirror sync health -->
|
||||||
|
<div class="vv-card" id="vv-pt-sync-card" style="margin-bottom:12px;">
|
||||||
|
<h3>Mirror Sync</h3>
|
||||||
|
<div id="vv-pt-sync-body" style="color:#444;font-size:12px;">Loading…</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<div class="vv-card" id="vv-pt-actions-card">
|
<div class="vv-card" id="vv-pt-actions-card">
|
||||||
<h3>Actions</h3>
|
<h3>Actions</h3>
|
||||||
<div id="vv-pt-actions-body" style="color:#444;font-size:12px;">Loading…</div>
|
<div id="vv-pt-actions-body" style="color:#444;font-size:12px;">Loading…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings -->
|
||||||
|
<div class="vv-card" id="vv-pt-settings-card" style="margin-top:12px;">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;cursor:pointer;"
|
||||||
|
onclick="vvPtToggleSettings()">
|
||||||
|
<h3 style="margin:0;">Settings</h3>
|
||||||
|
<div style="display:flex;align-items:center;gap:10px;">
|
||||||
|
<button id="vv-pt-settings-save" class="vv-pt-action-btn run"
|
||||||
|
onclick="event.stopPropagation();vvPtSaveSettings(this)"
|
||||||
|
style="display:none;">Save Changes</button>
|
||||||
|
<span id="vv-pt-settings-toggle"
|
||||||
|
style="font-size:11px;color:#7ab;background:#1a2a3a;border:1px solid #2e4a6b;
|
||||||
|
border-radius:4px;padding:3px 10px;white-space:nowrap;">▸ Show settings</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="vv-pt-settings-hint" style="font-size:10px;color:#444;margin-top:4px;">
|
||||||
|
Partnership configuration for this server.
|
||||||
|
</div>
|
||||||
|
<div id="vv-pt-settings-body" style="display:none;margin-top:10px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ── Toggle states — persist across 10s poll re-renders ────────────────────────
|
// ── Toggle states — persist across 10s poll re-renders ────────────────────────
|
||||||
const _vvOnboarding = {}; // hostId → true when onboard steps are expanded
|
const _vvOnboarding = {}; // hostId → true when onboard steps are expanded
|
||||||
@@ -96,6 +147,33 @@ function vvPtHideDeleteKeys(hostId) {
|
|||||||
if (_vvPtReload) _vvPtReload();
|
if (_vvPtReload) _vvPtReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vvApiKey(btn) {
|
||||||
|
const origText = btn.textContent;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '⟳ Working…';
|
||||||
|
fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now())
|
||||||
|
.then(r => {
|
||||||
|
if (!r.ok || r.status === 0) throw new Error('HTTP ' + r.status + ' ' + r.statusText);
|
||||||
|
return r.text();
|
||||||
|
})
|
||||||
|
.then(text => {
|
||||||
|
if (!text.trim()) throw new Error('Empty response from server');
|
||||||
|
return JSON.parse(text);
|
||||||
|
})
|
||||||
|
.then(d => {
|
||||||
|
if (d.ok) {
|
||||||
|
if (_vvPtReload) _vvPtReload();
|
||||||
|
} else {
|
||||||
|
alert('Failed: ' + (d.error ?? 'Unknown error'));
|
||||||
|
btn.disabled = false; btn.textContent = origText;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
alert('Error: ' + e);
|
||||||
|
btn.disabled = false; btn.textContent = origText;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function vvPtPhase2(btn, hostId) {
|
function vvPtPhase2(btn, hostId) {
|
||||||
if (!confirm(`Phase 2: Deploy containers + arr stack + establish partnership on ${hostId}?\n\nRequires ${hostId} to have Varaverk installed and SSH keys set up.`)) return;
|
if (!confirm(`Phase 2: Deploy containers + arr stack + establish partnership on ${hostId}?\n\nRequires ${hostId} to have Varaverk installed and SSH keys set up.`)) return;
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
@@ -175,6 +253,176 @@ function vvPtOffboard(btn) {
|
|||||||
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Offboard'; }, 4000));
|
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Offboard'; }, 4000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vvPtTransfer(btn, token) {
|
||||||
|
if (!confirm('Transfer ownership to the mirror?\n\nThis promotes the mirror to OWNER and demotes this server. '
|
||||||
|
+ 'partnership_transfer.sh requires sustained health checks before the switch completes.\n\nContinue?')) return;
|
||||||
|
if (prompt('Type YES to confirm ownership transfer:') !== 'YES') return;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '⟳ Transferring…';
|
||||||
|
_vvPtRun('Partnership/partnership_transfer.sh', '--confirm=' + token)
|
||||||
|
.then(d => { if (!d.ok) alert('Failed: ' + (d.error ?? 'Unknown error')); })
|
||||||
|
.catch(e => alert('Error: ' + e))
|
||||||
|
.finally(() => setTimeout(() => { btn.disabled = false; btn.textContent = '⇄ Transfer Ownership'; }, 4000));
|
||||||
|
}
|
||||||
|
|
||||||
|
function vvPtPing(btn, slot, idUpper) {
|
||||||
|
const out = document.getElementById('vv-pt-ping-' + idUpper);
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '⟳';
|
||||||
|
if (out) { out.textContent = ''; out.style.color = '#444'; }
|
||||||
|
fetch('/plugins/varaverk/api/partnership_ping.php?id=' + encodeURIComponent(slot) + '&_=' + Date.now())
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
if (out) {
|
||||||
|
if (d.ok) { out.textContent = '✓ ' + d.latency_ms + 'ms'; out.style.color = '#4caf50'; }
|
||||||
|
else { out.textContent = '✗ ' + (d.error || 'failed'); out.style.color = '#f44336'; }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => { if (out) { out.textContent = '✗ ' + e; out.style.color = '#f44336'; } })
|
||||||
|
.finally(() => { btn.disabled = false; btn.textContent = '⇄ Test'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function vvPtToggleSync(el, varName, enabled) {
|
||||||
|
el.style.pointerEvents = 'none';
|
||||||
|
el.style.opacity = '0.5';
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
|
||||||
|
name: varName,
|
||||||
|
enabled: String(enabled)
|
||||||
|
});
|
||||||
|
fetch('/plugins/varaverk/api/flag_toggle.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: params
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
if (d.ok) { if (_vvPtReload) _vvPtReload(); }
|
||||||
|
else { alert('Failed: ' + (d.error ?? 'Unknown error')); el.style.pointerEvents = ''; el.style.opacity = ''; }
|
||||||
|
})
|
||||||
|
.catch(e => { alert('Error: ' + e); el.style.pointerEvents = ''; el.style.opacity = ''; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Settings panel ─────────────────────────────────────────────────────────────
|
||||||
|
let _vvSetLoaded = false;
|
||||||
|
|
||||||
|
function _vvSetEsc(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function vvPtToggleSettings() {
|
||||||
|
const body = document.getElementById('vv-pt-settings-body');
|
||||||
|
const toggle = document.getElementById('vv-pt-settings-toggle');
|
||||||
|
const open = body.style.display !== 'none';
|
||||||
|
body.style.display = open ? 'none' : '';
|
||||||
|
toggle.textContent = open ? '▸ Show settings' : '▾ Hide settings';
|
||||||
|
if (!open && !_vvSetLoaded) vvPtLoadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
function vvPtLoadSettings() {
|
||||||
|
const body = document.getElementById('vv-pt-settings-body');
|
||||||
|
body.innerHTML = '<div style="color:#555;font-size:12px;padding:8px 0;">Loading…</div>';
|
||||||
|
fetch('/plugins/varaverk/api/partnership_settings.php?_=' + Date.now())
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
if (!d.ok || !(d.files || []).length) {
|
||||||
|
body.innerHTML = '<div style="color:#555;font-size:12px;padding:8px 0;">No settings available.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Host-aware hint: master.conf edits propagate; host config stays local.
|
||||||
|
const hasMaster = d.files.some(f => f.file === 'master.conf');
|
||||||
|
document.getElementById('vv-pt-settings-hint').textContent = hasMaster
|
||||||
|
? 'Shared (master.conf) edits propagate to partners on save · host config stays local to this server.'
|
||||||
|
: 'These settings apply to this host only.';
|
||||||
|
body.innerHTML = vvPtRenderSettings(d.files);
|
||||||
|
_vvSetLoaded = true;
|
||||||
|
})
|
||||||
|
.catch(e => { body.innerHTML = '<div style="color:#f88;font-size:12px;padding:8px 0;">Error: ' + e + '</div>'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function vvPtRenderSettings(files) {
|
||||||
|
let html = '';
|
||||||
|
for (const f of files) {
|
||||||
|
// One block per file; partnership scope yields a single section per file, so flatten.
|
||||||
|
const label = f.file === 'master.conf' ? 'master.conf — shared' : f.file + ' — this host';
|
||||||
|
html += `<div class="vv-set-file">
|
||||||
|
<div class="vv-set-file-hdr" onclick="vvPtToggleNode(this)">
|
||||||
|
<span>${_vvSetEsc(label)}</span><span class="vv-set-chev">▾</span>
|
||||||
|
</div>
|
||||||
|
<div class="vv-set-file-body">`;
|
||||||
|
for (const g of f.groups) {
|
||||||
|
for (const fld of g.fields) {
|
||||||
|
const attrs = `class="vv-set-input" data-key="${_vvSetEsc(fld.key)}" `
|
||||||
|
+ `data-file="${_vvSetEsc(fld.file)}" data-type="${_vvSetEsc(fld.type)}" `
|
||||||
|
+ `data-orig="${_vvSetEsc(fld.value)}" oninput="vvPtSetChanged(this)"`;
|
||||||
|
html += `<div class="vv-set-field">
|
||||||
|
<div class="vv-set-key">${_vvSetEsc(fld.key)}</div>`;
|
||||||
|
if (fld.desc) html += `<div class="vv-set-desc">${_vvSetEsc(fld.desc)}</div>`;
|
||||||
|
if (fld.type === 'scalar') {
|
||||||
|
html += `<input type="text" ${attrs} value="${_vvSetEsc(fld.value)}">`;
|
||||||
|
} else {
|
||||||
|
const rows = Math.min(16, (fld.value.match(/\n/g) || []).length + 2);
|
||||||
|
html += `<textarea ${attrs} rows="${rows}">${_vvSetEsc(fld.value)}</textarea>`;
|
||||||
|
}
|
||||||
|
html += `</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html += `</div></div>`;
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vvPtToggleNode(hdr) {
|
||||||
|
const body = hdr.nextElementSibling;
|
||||||
|
const chev = hdr.querySelector('.vv-set-chev');
|
||||||
|
const open = body.style.display !== 'none';
|
||||||
|
body.style.display = open ? 'none' : '';
|
||||||
|
if (chev) chev.textContent = open ? '▸' : '▾';
|
||||||
|
}
|
||||||
|
|
||||||
|
function vvPtSetChanged(el) {
|
||||||
|
const changed = el.value !== el.dataset.orig;
|
||||||
|
el.classList.toggle('changed', changed);
|
||||||
|
// Show Save button if any field is changed
|
||||||
|
const any = document.querySelector('#vv-pt-settings-body .vv-set-input.changed');
|
||||||
|
document.getElementById('vv-pt-settings-save').style.display = any ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function vvPtSaveSettings(btn) {
|
||||||
|
const changedEls = document.querySelectorAll('#vv-pt-settings-body .vv-set-input.changed');
|
||||||
|
if (!changedEls.length) return;
|
||||||
|
const changes = [];
|
||||||
|
changedEls.forEach(el => changes.push({
|
||||||
|
key: el.dataset.key, file: el.dataset.file, type: el.dataset.type, value: el.value
|
||||||
|
}));
|
||||||
|
if (!confirm(`Save ${changes.length} changed setting(s)?`)) return;
|
||||||
|
btn.disabled = true; btn.textContent = '⟳ Saving…';
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
csrf_token: typeof csrf_token !== 'undefined' ? csrf_token : '',
|
||||||
|
id: '__settings__',
|
||||||
|
changes: JSON.stringify(changes)
|
||||||
|
});
|
||||||
|
fetch('/plugins/varaverk/api/confform.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: params
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
btn.disabled = false;
|
||||||
|
if (d.ok) {
|
||||||
|
// Commit new originals, clear highlights
|
||||||
|
changedEls.forEach(el => { el.dataset.orig = el.value; el.classList.remove('changed'); });
|
||||||
|
btn.textContent = '✓ Saved';
|
||||||
|
setTimeout(() => { btn.textContent = 'Save Changes'; btn.style.display = 'none'; }, 2000);
|
||||||
|
} else {
|
||||||
|
btn.textContent = 'Save Changes';
|
||||||
|
alert('Save failed: ' + (d.error ?? 'Unknown error'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => { btn.disabled = false; btn.textContent = 'Save Changes'; alert('Error: ' + e); });
|
||||||
|
}
|
||||||
|
|
||||||
// ── Private page logic ─────────────────────────────────────────────────────────
|
// ── Private page logic ─────────────────────────────────────────────────────────
|
||||||
(function() {
|
(function() {
|
||||||
|
|
||||||
@@ -202,6 +450,91 @@ function _row(lbl, val) {
|
|||||||
return `<div class="vv-pt-row"><span class="vv-pt-lbl">${lbl}</span><span class="vv-pt-val">${val}</span></div>`;
|
return `<div class="vv-pt-row"><span class="vv-pt-lbl">${lbl}</span><span class="vv-pt-val">${val}</span></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Offline countdown warning ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function _renderOfflineWarn(cfg) {
|
||||||
|
const el = document.getElementById('vv-pt-offline-warn');
|
||||||
|
const days = cfg.offline_days;
|
||||||
|
const thr = cfg.offline_threshold || 30;
|
||||||
|
// Auto-offboard only runs for active partnerships — a stale counter while disabled is meaningless.
|
||||||
|
if (!cfg.enabled || !days || days < 1) { el.style.display = 'none'; el.innerHTML = ''; return; }
|
||||||
|
|
||||||
|
const left = Math.max(0, thr - days);
|
||||||
|
const crit = left <= 5;
|
||||||
|
const col = crit ? '#f44336' : '#ff9800';
|
||||||
|
const bg = crit ? '#2a1212' : '#1a1200';
|
||||||
|
const pct = Math.min(100, Math.round(days / thr * 100));
|
||||||
|
el.style.display = '';
|
||||||
|
el.innerHTML = `<div class="vv-card" style="background:${bg};border-color:${col};">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:baseline;gap:10px;flex-wrap:wrap;">
|
||||||
|
<span style="font-size:12px;color:${col};font-weight:600;">⚠ Partner unreachable ${days} day${days===1?'':'s'}</span>
|
||||||
|
<span style="font-size:11px;color:#888;">Auto-offboard in ${left} day${left===1?'':'s'} (threshold ${thr}d)</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:6px;height:5px;background:#000;border-radius:3px;overflow:hidden;">
|
||||||
|
<div style="width:${pct}%;height:100%;background:${col};"></div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mirror sync health ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function _syncToggle(gate, dimmed) {
|
||||||
|
if (!gate) return '';
|
||||||
|
const on = gate.on;
|
||||||
|
const col = on ? '#4caf50' : '#f44336';
|
||||||
|
const bg = on ? '#0a1a0a' : '#2a1212';
|
||||||
|
const op = dimmed ? 'opacity:.4;' : '';
|
||||||
|
return `<span onclick="vvPtToggleSync(this,'${gate.var}',${on ? 0 : 1})"
|
||||||
|
style="cursor:pointer;font-size:9px;font-weight:600;padding:2px 9px;border-radius:10px;
|
||||||
|
background:${bg};color:${col};border:1px solid ${col};white-space:nowrap;margin-left:8px;${op}"
|
||||||
|
title="${gate.var} — click to ${on ? 'disable' : 'enable'}">${on ? 'ON' : 'OFF'}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _renderSync(sync) {
|
||||||
|
const jobs = sync.jobs || {};
|
||||||
|
const gates = sync.gates || {};
|
||||||
|
const sCol = s => s === 'ok' ? '#4caf50' : s === 'running' ? '#4a9eff'
|
||||||
|
: s === 'warn' ? '#ff9800' : s === 'error' ? '#f44336' : '#555';
|
||||||
|
const sLbl = s => s === 'ok' ? '✓ ok' : s === 'running' ? '⟳ running'
|
||||||
|
: s === 'warn' ? '⚠ warn' : s === 'error' ? '✗ error' : '— never';
|
||||||
|
|
||||||
|
const globalOff = gates.global && !gates.global.on;
|
||||||
|
|
||||||
|
// Master toggle row — controls all mirroring (RSYNC_ENABLED, Tier 1).
|
||||||
|
let html = '';
|
||||||
|
if (gates.global) {
|
||||||
|
html += `<div class="vv-pt-row" style="margin-bottom:6px;padding-bottom:6px;border-bottom:1px solid #1c1c1c;">
|
||||||
|
<span class="vv-pt-lbl" style="font-weight:600;color:#999;">All mirroring</span>
|
||||||
|
<span class="vv-pt-val">${_syncToggle(gates.global, false)}</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-tier rows: status + last-run + per-tier toggle (dimmed when global is off).
|
||||||
|
const labels = {critical: 'Critical (30 min)', daily: 'Daily', weekly: 'Weekly'};
|
||||||
|
for (const key of ['critical', 'daily', 'weekly']) {
|
||||||
|
const j = jobs[key];
|
||||||
|
const status = j ? j.status : null;
|
||||||
|
const when = j && j.start
|
||||||
|
? (j.status === 'running' ? 'started ' + _relTime(j.start)
|
||||||
|
: _relTime(j.end || j.start))
|
||||||
|
: '—';
|
||||||
|
html += `<div class="vv-pt-row">
|
||||||
|
<span class="vv-pt-lbl">${labels[key]}</span>
|
||||||
|
<span class="vv-pt-val">
|
||||||
|
<span style="color:${sCol(status)};">${sLbl(status)}</span>
|
||||||
|
<span style="color:#444;font-size:10px;margin-left:6px;">${when}</span>
|
||||||
|
${_syncToggle(gates[key], globalOff)}
|
||||||
|
</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (globalOff) {
|
||||||
|
html += `<div style="font-size:10px;color:#f44336;margin-top:6px;">⚠ All mirroring is off — per-tier switches have no effect until re-enabled.</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Config bar ────────────────────────────────────────────────────────────────
|
// ── Config bar ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function _renderConfig(cfg) {
|
function _renderConfig(cfg) {
|
||||||
@@ -286,9 +619,56 @@ function _nodeCard(node) {
|
|||||||
node.key_ready ? `<div style="font-size:10px;color:#ff9800;margin-bottom:6px;padding:2px 6px;background:#1a1200;border:1px solid #3a2800;border-radius:3px;">🔑 Key generated · install on HOST2 then Push Conf</div>` :
|
node.key_ready ? `<div style="font-size:10px;color:#ff9800;margin-bottom:6px;padding:2px 6px;background:#1a1200;border:1px solid #3a2800;border-radius:3px;">🔑 Key generated · install on HOST2 then Push Conf</div>` :
|
||||||
`<div style="font-size:10px;color:#555;margin-bottom:6px;padding:2px 6px;background:#111;border:1px solid #222;border-radius:3px;">○ Not yet provisioned</div>`);
|
`<div style="font-size:10px;color:#555;margin-bottom:6px;padding:2px 6px;background:#111;border:1px solid #222;border-radius:3px;">○ Not yet provisioned</div>`);
|
||||||
|
|
||||||
|
const apiKeyHtml = node.is_me
|
||||||
|
? (node.api_key_set
|
||||||
|
? `<div style="font-size:10px;color:#2a4a2a;margin-bottom:6px;padding:2px 6px;background:#0a1a0a;
|
||||||
|
border:1px solid #1a3a1a;border-radius:3px;display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<span style="color:#3a7a3a;">🔑 API key: <span style="color:#4caf50;">${node.api_key_preview}</span></span>
|
||||||
|
<button onclick="vvApiKey(this)"
|
||||||
|
style="font-size:9px;padding:1px 6px;background:#0d0d0d;color:#444;border:1px solid #222;
|
||||||
|
border-radius:2px;cursor:pointer;margin-left:6px;white-space:nowrap;">↻ renew</button>
|
||||||
|
</div>`
|
||||||
|
: `<div style="font-size:10px;color:#ff9800;margin-bottom:6px;padding:2px 6px;background:#1a1200;
|
||||||
|
border:1px solid #3a2800;border-radius:3px;display:flex;justify-content:space-between;align-items:center;">
|
||||||
|
<span>⚡ No API key — Monitor stats unavailable</span>
|
||||||
|
<button onclick="vvApiKey(this)"
|
||||||
|
style="font-size:9px;padding:1px 8px;background:#1a3a1a;color:#6fcf97;border:1px solid #2e6b2e;
|
||||||
|
border-radius:2px;cursor:pointer;margin-left:6px;white-space:nowrap;">+ Create</button>
|
||||||
|
</div>`)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// ── Metrics strip ────────────────────────────────────────────────────────────
|
||||||
|
const m = node.metrics || {};
|
||||||
|
function _ptCol(v, warn, crit) {
|
||||||
|
return v >= crit ? '#f44336' : v >= warn ? '#ff9800' : '#4caf50';
|
||||||
|
}
|
||||||
|
const metricPairs = [];
|
||||||
|
if (m.cpu_pct != null) metricPairs.push(['CPU', `<span style="color:${_ptCol(m.cpu_pct,70,90)};">${m.cpu_pct}%</span>`]);
|
||||||
|
if (m.ram_used_gb != null) {
|
||||||
|
const ramPct = m.ram_total_gb ? Math.round(m.ram_used_gb / m.ram_total_gb * 100) : 0;
|
||||||
|
metricPairs.push(['RAM', `<span style="color:${_ptCol(ramPct,75,90)};">${m.ram_used_gb}<span style="color:#444;font-size:9px;">/${m.ram_total_gb}GB</span></span>`]);
|
||||||
|
}
|
||||||
|
if (m.load_avg != null) metricPairs.push(['Load', `<span style="color:${m.load_avg>8?'#f44336':m.load_avg>4?'#ff9800':'#4caf50'};">${m.load_avg}</span>`]);
|
||||||
|
if (m.containers != null) metricPairs.push(['Conts', `<span style="color:#bbb;">${m.containers}</span>`]);
|
||||||
|
if (m.array_used_tb != null) metricPairs.push(['Array', `<span style="color:#bbb;">${m.array_used_tb}<span style="color:#444;font-size:9px;">/${m.array_total_tb}TB</span></span>`]);
|
||||||
|
if (m.max_disk_temp != null) metricPairs.push(['Temp', `<span style="color:${_ptCol(m.max_disk_temp,55,70)};">${m.max_disk_temp}°C</span>`]);
|
||||||
|
if (m.vm_count != null && m.vm_count > 0) metricPairs.push(['VMs', `<span style="color:#bbb;">${m.vm_count}</span>`]);
|
||||||
|
|
||||||
|
let metricsHtml = '';
|
||||||
|
if (metricPairs.length) {
|
||||||
|
let cells = '';
|
||||||
|
for (const [lbl, val] of metricPairs) {
|
||||||
|
cells += `<span style="color:#555;">${lbl}</span>${val}`;
|
||||||
|
}
|
||||||
|
if (metricPairs.length % 2 !== 0) cells += '<span></span><span></span>';
|
||||||
|
metricsHtml = `<div style="display:grid;grid-template-columns:auto 1fr auto 1fr;gap:2px 8px;font-size:10px;margin-bottom:6px;">${cells}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
let body = '';
|
let body = '';
|
||||||
|
|
||||||
body += phaseHtml;
|
body += phaseHtml;
|
||||||
|
body += apiKeyHtml;
|
||||||
|
body += metricsHtml;
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
body += `<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
|
body += `<div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;">
|
||||||
@@ -337,7 +717,7 @@ function _renderActions(nodes, cfg) {
|
|||||||
const remotes = nodes.filter(n => !n.is_me);
|
const remotes = nodes.filter(n => !n.is_me);
|
||||||
const hasPartner = remotes.some(n => n.hostname);
|
const hasPartner = remotes.some(n => n.hostname);
|
||||||
const termBase = `https://${window.location.hostname}/webterminal/ttyd/`;
|
const termBase = `https://${window.location.hostname}/webterminal/ttyd/`;
|
||||||
const termCmd = `bash /mnt/user/appdata/Varaverk/Partnership/partnership_onboard.sh --phase1-only`;
|
const termCmd = `bash <?= SCRIPTS_DIR ?>/Partnership/partnership_onboard.sh --phase1-only`;
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
|
|
||||||
@@ -365,13 +745,18 @@ function _renderActions(nodes, cfg) {
|
|||||||
|
|
||||||
html += `<div style="margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid #1e1e1e;">`;
|
html += `<div style="margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid #1e1e1e;">`;
|
||||||
|
|
||||||
// Header: host + online dot
|
// Header: host + online dot + test connection
|
||||||
html += `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
html += `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||||||
<div>
|
<div>
|
||||||
<span style="font-size:10px;color:#444;">${remote.id}</span>
|
<span style="font-size:10px;color:#444;">${remote.id}</span>
|
||||||
<span style="font-size:12px;color:#bbb;font-weight:500;margin-left:5px;">${remote.hostname}</span>
|
<span style="font-size:12px;color:#bbb;font-weight:500;margin-left:5px;">${remote.hostname}</span>
|
||||||
</div>
|
</div>
|
||||||
<span style="font-size:10px;color:${dotCol};">● ${dotLbl}</span>
|
<div style="display:flex;align-items:center;gap:8px;">
|
||||||
|
<span id="vv-pt-ping-${remote.id}" style="font-size:10px;color:#444;"></span>
|
||||||
|
<button class="vv-pt-action-btn info" onclick="vvPtPing(this,'${remote.slot}','${remote.id}')"
|
||||||
|
style="font-size:10px;padding:2px 8px;" title="SSH echo round-trip">⇄ Test</button>
|
||||||
|
<span style="font-size:10px;color:${dotCol};">● ${dotLbl}</span>
|
||||||
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const isDeleting = !!_vvDeleteKeys[remote.id];
|
const isDeleting = !!_vvDeleteKeys[remote.id];
|
||||||
@@ -516,10 +901,18 @@ function _renderActions(nodes, cfg) {
|
|||||||
</button></div>`;
|
</button></div>`;
|
||||||
|
|
||||||
if (cfg.enabled && isOwner) {
|
if (cfg.enabled && isOwner) {
|
||||||
|
const tok = (cfg.transfer_confirm || 'i-understand-this-transfers-ownership')
|
||||||
|
.replace(/'/g, "\\'");
|
||||||
html += `<div style="margin-top:12px;padding-top:10px;border-top:1px solid #1e1e1e;">
|
html += `<div style="margin-top:12px;padding-top:10px;border-top:1px solid #1e1e1e;">
|
||||||
<span style="font-size:11px;color:#555;">Transfer ownership — run manually:</span>
|
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;">
|
||||||
<div class="vv-pt-transfer-note" style="margin-top:4px;padding:6px 10px;background:#111;border-radius:4px;color:#555;">
|
<span style="font-size:11px;color:#555;">Transfer ownership to the mirror — promotes it to owner.</span>
|
||||||
bash Partnership/partnership_transfer.sh --confirm=i-understand-this-transfers-ownership
|
<button class="vv-pt-action-btn warn" onclick="vvPtTransfer(this, '${tok}')"
|
||||||
|
title="Runs partnership_transfer.sh — requires sustained health checks before switching">
|
||||||
|
⇄ Transfer Ownership
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="vv-pt-transfer-note" style="margin-top:6px;padding:6px 10px;background:#111;border-radius:4px;color:#444;">
|
||||||
|
Or run manually: bash Partnership/partnership_transfer.sh --confirm=${cfg.transfer_confirm || 'i-understand-this-transfers-ownership'}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -536,6 +929,12 @@ function _render(data) {
|
|||||||
// Config bar
|
// Config bar
|
||||||
document.getElementById('vv-pt-config-body').innerHTML = _renderConfig(cfg);
|
document.getElementById('vv-pt-config-body').innerHTML = _renderConfig(cfg);
|
||||||
|
|
||||||
|
// Offline countdown warning
|
||||||
|
_renderOfflineWarn(cfg);
|
||||||
|
|
||||||
|
// Mirror sync health
|
||||||
|
document.getElementById('vv-pt-sync-body').innerHTML = _renderSync(data.sync || {});
|
||||||
|
|
||||||
// Node grid — columns based on count
|
// Node grid — columns based on count
|
||||||
const cols = nodes.length <= 2 ? nodes.length : nodes.length <= 4 ? 2 : 3;
|
const cols = nodes.length <= 2 ? nodes.length : nodes.length <= 4 ? 2 : 3;
|
||||||
const grid = document.getElementById('vv-pt-nodes');
|
const grid = document.getElementById('vv-pt-nodes');
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ require_once dirname(__DIR__) . '/include/scheduler.php';
|
|||||||
// Setup mode — auto-open a conf file and force the editing sequence
|
// Setup mode — auto-open a conf file and force the editing sequence
|
||||||
$vv_setup_conf = preg_match('/^[\w.]+\.conf$/', $_GET['vv_setup'] ?? '')
|
$vv_setup_conf = preg_match('/^[\w.]+\.conf$/', $_GET['vv_setup'] ?? '')
|
||||||
? $_GET['vv_setup'] : '';
|
? $_GET['vv_setup'] : '';
|
||||||
|
|
||||||
|
// Deep-link from Monitor → open a specific script on load (e.g. ?tab=scheduler&vv_script=Media/x.sh)
|
||||||
|
$vv_open_script = preg_match('#^[\w./-]+\.sh$#', $_GET['vv_script'] ?? '')
|
||||||
|
? $_GET['vv_script'] : '';
|
||||||
$_vv_my_host = vv_detect_host();
|
$_vv_my_host = vv_detect_host();
|
||||||
$_vv_local_host_conf = ($_vv_my_host !== 'unknown') ? $_vv_my_host . '.conf' : 'host1.conf';
|
$_vv_local_host_conf = ($_vv_my_host !== 'unknown') ? $_vv_my_host . '.conf' : 'host1.conf';
|
||||||
|
|
||||||
@@ -959,6 +963,20 @@ if (vvSetupConf) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deep-link from Monitor: open a specific script's panel once the tree is rendered.
|
||||||
|
const vvOpenScriptId = <?= json_encode($vv_open_script) ?>;
|
||||||
|
if (vvOpenScriptId) {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.querySelector('[data-id="' + CSS.escape(vvOpenScriptId) + '"]')) {
|
||||||
|
vvOpenRight(vvOpenScriptId);
|
||||||
|
const row = document.querySelector('[data-id="' + CSS.escape(vvOpenScriptId) + '"]');
|
||||||
|
if (row) row.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function vvEscHtml(s) {
|
function vvEscHtml(s) {
|
||||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
}
|
}
|
||||||
@@ -1995,13 +2013,13 @@ function vvUpdateErrors(errors) {
|
|||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
let html = '<div class="vv-errors-list">';
|
let html = '<div class="vv-errors-list">';
|
||||||
for (const e of unacked) {
|
for (const e of unacked) {
|
||||||
const jobId = JSON.stringify(e.script + '.sh');
|
const jobId = "'" + (e.script + '.sh').replace(/\\/g,"\\\\").replace(/'/g,"\\'") + "'";
|
||||||
const label = e.script.split('/').pop().replace(/_/g, ' ');
|
const label = e.script.split('/').pop().replace(/_/g, ' ');
|
||||||
html += '<div class="vv-err-row">'
|
html += '<div class="vv-err-row">'
|
||||||
+ '<div class="vv-err-top">'
|
+ '<div class="vv-err-top">'
|
||||||
+ '<span class="vv-err-script" onclick="vvOpenRight(' + jobId + ')" style="cursor:pointer" title="' + vvEscHtml(e.script) + '">' + vvEscHtml(label) + '</span>'
|
+ '<span class="vv-err-script" onclick="vvOpenRight(' + jobId + ')" style="cursor:pointer" title="' + vvEscHtml(e.script) + '">' + vvEscHtml(label) + '</span>'
|
||||||
+ '<span class="vv-err-age">' + vvFmtAge(now - e.ts) + '</span>'
|
+ '<span class="vv-err-age">' + vvFmtAge(now - e.ts) + '</span>'
|
||||||
+ '<button class="vv-btn-sm vv-ack-btn" onclick="vvAckError(' + JSON.stringify(e.script) + ',' + e.ts + ',this)">Ack</button>'
|
+ '<button class="vv-btn-sm vv-ack-btn" onclick="vvAckError(\'' + e.script.replace(/\\/g,"\\\\").replace(/'/g,"\\'") + "'," + e.ts + ',this)">Ack</button>'
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
+ '<div class="vv-err-line">' + vvEscHtml(e.line) + '</div>'
|
+ '<div class="vv-err-line">' + vvEscHtml(e.line) + '</div>'
|
||||||
+ '</div>';
|
+ '</div>';
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ $isConfOnlyFlow = !empty($masterHost1) && $confMissing;
|
|||||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: params
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: params
|
||||||
}).then(r => r.json()).then(d => {
|
}).then(r => r.json()).then(d => {
|
||||||
if (d.ok) {
|
if (d.ok) {
|
||||||
status.textContent = '✓ Created — loading…'; status.className = 'ok';
|
status.textContent = '✓ Created'; status.className = 'ok';
|
||||||
setTimeout(() => { window.location.href = '?tab=scheduler&vv_setup=' + encodeURIComponent(<?= json_encode($myHostId . '.conf') ?>); }, 600);
|
vvShowStep2('?tab=scheduler&vv_setup=' + encodeURIComponent(<?= json_encode($myHostId . '.conf') ?>));
|
||||||
} else {
|
} else {
|
||||||
btn.disabled = false; btn.textContent = 'Create <?= htmlspecialchars($myHostId) ?>.conf and continue →';
|
btn.disabled = false; btn.textContent = 'Create <?= htmlspecialchars($myHostId) ?>.conf and continue →';
|
||||||
status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err';
|
status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err';
|
||||||
@@ -151,8 +151,8 @@ $isConfOnlyFlow = !empty($masterHost1) && $confMissing;
|
|||||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: params
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: params
|
||||||
}).then(r => r.json()).then(d => {
|
}).then(r => r.json()).then(d => {
|
||||||
if (d.ok) {
|
if (d.ok) {
|
||||||
status.textContent = '✓ Configuration pulled — loading…'; status.className = 'ok';
|
status.textContent = '✓ Configuration pulled'; status.className = 'ok';
|
||||||
setTimeout(() => { window.location.href = d.redirect ?? '?tab=scheduler'; }, 600);
|
vvShowStep2(d.redirect ?? '?tab=scheduler');
|
||||||
} else {
|
} else {
|
||||||
btn.disabled = false; btn.textContent = 'Pull configuration from HOST1 →';
|
btn.disabled = false; btn.textContent = 'Pull configuration from HOST1 →';
|
||||||
status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err';
|
status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err';
|
||||||
@@ -244,8 +244,8 @@ $isConfOnlyFlow = !empty($masterHost1) && $confMissing;
|
|||||||
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: params
|
method: 'POST', headers: {'Content-Type': 'application/x-www-form-urlencoded'}, body: params
|
||||||
}).then(r => r.json()).then(d => {
|
}).then(r => r.json()).then(d => {
|
||||||
if (d.ok) {
|
if (d.ok) {
|
||||||
status.textContent = '✓ Saved — loading…'; status.className = 'ok';
|
status.textContent = '✓ Saved'; status.className = 'ok';
|
||||||
setTimeout(() => { window.location.href = d.redirect ?? '?tab=scheduler&vv_setup=master.conf'; }, 600);
|
vvShowStep2(d.redirect ?? '?tab=scheduler&vv_setup=master.conf');
|
||||||
} else {
|
} else {
|
||||||
btn.disabled = false; btn.textContent = 'Save and continue →';
|
btn.disabled = false; btn.textContent = 'Save and continue →';
|
||||||
status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err';
|
status.textContent = '✗ ' + (d.error ?? 'Error'); status.className = 'err';
|
||||||
@@ -255,4 +255,58 @@ $isConfOnlyFlow = !empty($masterHost1) && $confMissing;
|
|||||||
</script>
|
</script>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Step 2: API key — shown after any wizard flow completes -->
|
||||||
|
<div id="vv-setup-step2" style="display:none;">
|
||||||
|
<hr class="vv-setup-divider">
|
||||||
|
<div style="font-size:10px;color:#555;text-transform:uppercase;letter-spacing:.06em;margin-bottom:10px;">Step 2 of 2 — Unraid API Key</div>
|
||||||
|
<div class="vv-setup-info-box">
|
||||||
|
Varaverk uses the local Unraid API to display live stats on the Monitor tab.
|
||||||
|
Creates a <strong>Varaverk</strong> key via <code style="color:#555;">unraid-api</code> and writes it to your host conf.
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;gap:10px;align-items:center;">
|
||||||
|
<button id="vv-key-btn2" onclick="vvCreateApiKeyWizard(this)"
|
||||||
|
style="flex:1;padding:10px 0;background:#1a3a1a;border:1px solid #2e6b2e;color:#6fcf97;
|
||||||
|
font-family:monospace;font-size:13px;border-radius:3px;cursor:pointer;">
|
||||||
|
Create API Key
|
||||||
|
</button>
|
||||||
|
<a id="vv-skip-link" href="#" onclick="vvWizardContinue(event)"
|
||||||
|
style="font-size:11px;color:#444;text-decoration:none;white-space:nowrap;">Skip →</a>
|
||||||
|
</div>
|
||||||
|
<div id="vv-key-status2" style="margin-top:8px;font-size:12px;min-height:16px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let _vvWizardNext = '';
|
||||||
|
function vvShowStep2(redirect) {
|
||||||
|
_vvWizardNext = redirect;
|
||||||
|
document.getElementById('vv-setup-step2').style.display = 'block';
|
||||||
|
}
|
||||||
|
function vvWizardContinue(e) {
|
||||||
|
if (e) e.preventDefault();
|
||||||
|
window.location.href = _vvWizardNext || '?tab=scheduler';
|
||||||
|
}
|
||||||
|
function vvCreateApiKeyWizard(btn) {
|
||||||
|
const status = document.getElementById('vv-key-status2');
|
||||||
|
btn.disabled = true; btn.textContent = '⟳ Creating…';
|
||||||
|
fetch('/plugins/varaverk/api/create_api_key.php?_=' + Date.now())
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(d => {
|
||||||
|
if (d.ok) {
|
||||||
|
status.textContent = '✓ Key created — ' + d.key_preview; status.style.color = '#4a8';
|
||||||
|
btn.textContent = 'Continue →'; btn.disabled = false;
|
||||||
|
btn.onclick = vvWizardContinue;
|
||||||
|
const skip = document.getElementById('vv-skip-link');
|
||||||
|
if (skip) skip.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
status.textContent = '✗ ' + (d.error ?? 'Failed'); status.style.color = '#a44';
|
||||||
|
btn.disabled = false; btn.textContent = 'Retry';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
status.textContent = '✗ ' + e; status.style.color = '#a44';
|
||||||
|
btn.disabled = false; btn.textContent = 'Retry';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+141
-131
@@ -2,18 +2,26 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "varaverk">
|
<!ENTITY name "varaverk">
|
||||||
<!ENTITY author "gmer4lfe">
|
<!ENTITY author "gmer4lfe">
|
||||||
<!ENTITY version "2026.05.30">
|
<!ENTITY version "2026.05.31">
|
||||||
|
<!ENTITY sha256 "932468c8a114134dea3062f2810a7ca5a7b1168e1e78d0195c41c61b86db7728">
|
||||||
<!ENTITY launch "varaverk/monitor">
|
<!ENTITY launch "varaverk/monitor">
|
||||||
<!ENTITY github "https://github.com/FailedProxy/Varaverk">
|
<!ENTITY github "https://github.com/FailedProxy/Varaverk">
|
||||||
<!ENTITY branch "main">
|
<!ENTITY branch "main">
|
||||||
<!ENTITY cfgdir "/boot/config/plugins/varaverk">
|
<!ENTITY cfgdir "/boot/config/plugins/varaverk">
|
||||||
<!ENTITY plugdir "/usr/local/emhttp/plugins/varaverk">
|
<!ENTITY plugdir "/usr/local/emhttp/plugins/varaverk">
|
||||||
|
<!ENTITY pkg "varaverk-&version;-noarch-1.txz">
|
||||||
]>
|
]>
|
||||||
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;"
|
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;"
|
||||||
support="https://github.com/FailedProxy/Varaverk/issues"
|
support="https://github.com/FailedProxy/Varaverk/issues"
|
||||||
icon="/plugins/varaverk/icons/varaverk.png">
|
icon="/plugins/varaverk/icons/varaverk.png">
|
||||||
|
|
||||||
<CHANGES>
|
<CHANGES>
|
||||||
|
###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
|
###2026.05.30
|
||||||
- First-run setup wizard: auto-detects hostname, creates master.conf + host conf from templates
|
- 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
|
- Scheduler setup mode: after wizard, master.conf and host conf open sequentially with forced save flow
|
||||||
@@ -33,143 +41,153 @@
|
|||||||
</CHANGES>
|
</CHANGES>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
── Install / Update ────────────────────────────────────────────────────────────
|
── 1. Web files package (runs on every boot) ──────────────────────────────────
|
||||||
Runs on first install and on every plugin update.
|
unRAID runs `plugin install` on every .plg at boot, BEFORE the array mounts.
|
||||||
Clones the repo on fresh install; pulls latest on update.
|
The .txz lives on flash, so this installs the PHP/JS UI to RAM in time — and the
|
||||||
Array must be started — appdata must be available.
|
disks_mounted event hooks inside it are then present when the array starts, so
|
||||||
|
cron is rebuilt automatically. No Method attr = treated as "install" = every boot.
|
||||||
|
|
||||||
|
Download is skipped when the .txz is already on flash with a matching SHA256, so
|
||||||
|
this also works offline / for a local install (copy the .txz into &cfgdir;).
|
||||||
────────────────────────────────────────────────────────────────────────────────
|
────────────────────────────────────────────────────────────────────────────────
|
||||||
-->
|
-->
|
||||||
<FILE Name="&cfgdir;/install.sh" Run="/bin/bash" Method="install update">
|
<FILE Name="&cfgdir;/&pkg;" Run="/sbin/upgradepkg --install-new --reinstall">
|
||||||
|
<URL>https://github.com/FailedProxy/Varaverk/releases/download/&version;/&pkg;</URL>
|
||||||
|
<SHA256>&sha256;</SHA256>
|
||||||
|
</FILE>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
── 2. Scripts bootstrap (first install only) ──────────────────────────────────
|
||||||
|
Clones the repo directly into the plugin config dir on flash (/boot/config/plugins/varaverk).
|
||||||
|
No array dependency — scripts live on flash (64GB NVMe) and are available at boot.
|
||||||
|
Uses git init+fetch+reset so the clone works into the non-empty cfgdir (varaverk.cfg,
|
||||||
|
varaverk-*.txz etc. are already there). Never auto-pulls — updates via the UI git pull.
|
||||||
|
|
||||||
|
Clone source priority:
|
||||||
|
1. Gitea (internal) — reads settings from varaverk.cfg; detects container IP at runtime
|
||||||
|
2. GitHub (public) — HTTPS fallback if Gitea is unreachable
|
||||||
|
────────────────────────────────────────────────────────────────────────────────
|
||||||
|
-->
|
||||||
|
<FILE Run="/bin/bash" Method="install">
|
||||||
<INLINE>
|
<INLINE>
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
PLUGIN="varaverk"
|
PLUGIN="varaverk"
|
||||||
GITHUB="https://github.com/FailedProxy/Varaverk"
|
|
||||||
BRANCH="main"
|
|
||||||
CFG_DIR="/boot/config/plugins/$PLUGIN"
|
CFG_DIR="/boot/config/plugins/$PLUGIN"
|
||||||
CFG_FILE="$CFG_DIR/varaverk.cfg"
|
CFG_FILE="$CFG_DIR/varaverk.cfg"
|
||||||
WEB_DIR="/usr/local/emhttp/plugins/$PLUGIN"
|
GITHUB="https://github.com/FailedProxy/Varaverk"
|
||||||
|
BRANCH="main"
|
||||||
LOG="$CFG_DIR/install.log"
|
LOG="$CFG_DIR/install.log"
|
||||||
|
|
||||||
mkdir -p "$CFG_DIR"
|
mkdir -p "$CFG_DIR"
|
||||||
|
|
||||||
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
|
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
|
||||||
|
|
||||||
log "=== Varaverk install/update ==="
|
# Scripts live in the plugin dir on flash — no array needed.
|
||||||
|
SCRIPTS_DIR="$CFG_DIR"
|
||||||
# Unraid only
|
|
||||||
if [[ ! -f /etc/unraid-version ]]; then
|
|
||||||
log "ERROR: not running on Unraid — aborting"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Read SCRIPTS_DIR from cfg if present
|
|
||||||
if [[ -f "$CFG_FILE" ]]; then
|
|
||||||
_sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/dev/null)
|
|
||||||
[[ -n "$_sd" ]] && SCRIPTS_DIR="$_sd"
|
|
||||||
fi
|
|
||||||
SCRIPTS_DIR="${SCRIPTS_DIR:-/mnt/user/appdata/Varaverk}"
|
|
||||||
CONF_DIR="$SCRIPTS_DIR/Configurations"
|
CONF_DIR="$SCRIPTS_DIR/Configurations"
|
||||||
|
|
||||||
# Array must be started — appdata must be available
|
# Seed varaverk.cfg with defaults (SCRIPTS_DIR + Gitea settings) if not present.
|
||||||
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
|
|
||||||
if [[ ! -f "$CFG_FILE" ]]; then
|
if [[ ! -f "$CFG_FILE" ]]; then
|
||||||
printf 'SCRIPTS_DIR="%s"\n' "$SCRIPTS_DIR" > "$CFG_FILE"
|
cat > "$CFG_FILE" <<'CFGEOF'
|
||||||
log "Created varaverk.cfg (SCRIPTS_DIR=$SCRIPTS_DIR)"
|
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
|
fi
|
||||||
|
|
||||||
# Create Configurations dir if git clone didn't
|
# Read Gitea settings from varaverk.cfg (allows override without editing .plg).
|
||||||
mkdir -p "$CONF_DIR"
|
_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
|
# Clone on first install only; never auto-pull (updates via the UI git pull).
|
||||||
if [[ ! -f "$CONF_DIR/master.conf" ]]; then
|
if [[ ! -d "$SCRIPTS_DIR/.git" ]]; then
|
||||||
if [[ -f "$SCRIPTS_DIR/Configurations/master.conf.template" ]]; then
|
log "initialising repo in $SCRIPTS_DIR ($BRANCH)..."
|
||||||
cp "$SCRIPTS_DIR/Configurations/master.conf.template" "$CONF_DIR/master.conf"
|
|
||||||
log "Created master.conf from template — fill in HOST1 and HOST2 to continue"
|
# Locate Gitea: local container → local IP; else Tailscale; else fall back to GitHub.
|
||||||
else
|
GITEA_IP=""
|
||||||
log "WARNING: master.conf.template not found — master.conf not created"
|
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
|
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
|
fi
|
||||||
|
|
||||||
log "Install complete — open Varaverk in Unraid to complete setup"
|
# Seed master.conf from template if absent.
|
||||||
log "→ Plugins → Varaverk (or navigate to Settings → Utilities → Varaverk)"
|
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"
|
||||||
]]>
|
]]>
|
||||||
</INLINE>
|
</INLINE>
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
── Boot (every boot) ───────────────────────────────────────────────────────────
|
── 3. Remove ───────────────────────────────────────────────────────────────────
|
||||||
Fast idempotent check — recreates the web symlink if it disappeared.
|
Stops background scripts, removes cron, the installed package, and flash config.
|
||||||
No git operations. Safe to run before the array is fully started.
|
Scripts/conf in appdata are left intact (delete manually for a full wipe).
|
||||||
────────────────────────────────────────────────────────────────────────────────
|
────────────────────────────────────────────────────────────────────────────────
|
||||||
-->
|
-->
|
||||||
<FILE Name="&cfgdir;/boot.sh" Run="/bin/bash">
|
<FILE Run="/bin/bash" Method="remove">
|
||||||
<INLINE>
|
|
||||||
<![CDATA[
|
|
||||||
#!/bin/bash
|
|
||||||
PLUGIN="varaverk"
|
|
||||||
CFG_FILE="/boot/config/plugins/$PLUGIN/varaverk.cfg"
|
|
||||||
WEB_DIR="/usr/local/emhttp/plugins/$PLUGIN"
|
|
||||||
|
|
||||||
# Read SCRIPTS_DIR
|
|
||||||
if [[ -f "$CFG_FILE" ]]; then
|
|
||||||
_sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/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
|
|
||||||
]]>
|
|
||||||
</INLINE>
|
|
||||||
</FILE>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
── Remove ──────────────────────────────────────────────────────────────────────
|
|
||||||
Removes the web symlink. Scripts and conf in appdata are left intact.
|
|
||||||
────────────────────────────────────────────────────────────────────────────────
|
|
||||||
-->
|
|
||||||
<FILE Name="&cfgdir;/remove.sh" Run="/bin/bash" Method="remove">
|
|
||||||
<INLINE>
|
<INLINE>
|
||||||
<![CDATA[
|
<![CDATA[
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
@@ -178,44 +196,36 @@ CFG_DIR="/boot/config/plugins/$PLUGIN"
|
|||||||
CFG_FILE="$CFG_DIR/varaverk.cfg"
|
CFG_FILE="$CFG_DIR/varaverk.cfg"
|
||||||
CRON_FILE="$CFG_DIR/varaverk.cron"
|
CRON_FILE="$CFG_DIR/varaverk.cron"
|
||||||
WEB_DIR="/usr/local/emhttp/plugins/$PLUGIN"
|
WEB_DIR="/usr/local/emhttp/plugins/$PLUGIN"
|
||||||
|
|
||||||
log() { echo "[Varaverk remove] $*"; }
|
log() { echo "[Varaverk remove] $*"; }
|
||||||
|
|
||||||
# Read SCRIPTS_DIR so we can locate running scripts
|
[[ -f "$CFG_FILE" ]] && _sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/dev/null)
|
||||||
if [[ -f "$CFG_FILE" ]]; then
|
SCRIPTS_DIR="${_sd:-$CFG_DIR}"
|
||||||
_sd=$(grep -oP '(?<=SCRIPTS_DIR=")[^"]+' "$CFG_FILE" 2>/dev/null)
|
|
||||||
fi
|
|
||||||
SCRIPTS_DIR="${_sd:-/mnt/user/appdata/Varaverk}"
|
|
||||||
|
|
||||||
# ── Stop continuous background scripts ───────────────────────────────────────
|
# Stop continuous background scripts.
|
||||||
# fallback.sh manages its own lock — use --stop for clean shutdown
|
|
||||||
if [[ -f "$SCRIPTS_DIR/Fallback/fallback.sh" ]]; then
|
if [[ -f "$SCRIPTS_DIR/Fallback/fallback.sh" ]]; then
|
||||||
bash "$SCRIPTS_DIR/Fallback/fallback.sh" --stop 2>/dev/null && \
|
bash "$SCRIPTS_DIR/Fallback/fallback.sh" --stop 2>/dev/null && log "fallback.sh stopped" || true
|
||||||
log "fallback.sh stopped" || true
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Kill any remaining Varaverk background processes (watchdog orchestrator, etc.)
|
|
||||||
pkill -f "run_job.sh" 2>/dev/null || true
|
pkill -f "run_job.sh" 2>/dev/null || true
|
||||||
pkill -f "watchdog_orchestrator.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
|
if [[ -f "$CRON_FILE" ]]; then
|
||||||
rm -f "$CRON_FILE"
|
rm -f "$CRON_FILE"
|
||||||
/usr/local/sbin/update_cron 2>/dev/null || true
|
/usr/local/sbin/update_cron 2>/dev/null || true
|
||||||
log "Cron entries removed"
|
log "cron removed"
|
||||||
fi
|
fi
|
||||||
# Remove legacy direct cron file if it exists
|
|
||||||
rm -f /etc/cron.d/varaverk
|
rm -f /etc/cron.d/varaverk
|
||||||
|
|
||||||
# ── Remove web symlink ────────────────────────────────────────────────────────
|
# Remove the installed package (and its RAM files).
|
||||||
[[ -L "$WEB_DIR" ]] && rm -f "$WEB_DIR" && log "Web symlink removed"
|
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"
|
rm -rf "$CFG_DIR"
|
||||||
log "Plugin config removed from flash"
|
log "flash config removed"
|
||||||
|
log "done — scripts/conf in $SCRIPTS_DIR preserved (delete manually for full wipe)"
|
||||||
log "Done. Scripts and conf in $SCRIPTS_DIR are preserved."
|
|
||||||
log "To fully remove: delete $SCRIPTS_DIR manually."
|
|
||||||
]]>
|
]]>
|
||||||
</INLINE>
|
</INLINE>
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|||||||
+1
-1
@@ -184,7 +184,7 @@ if ! check_rsync_enabled; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Blocklist gate — refuse to sync with a partner blocked after offboard
|
# 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
|
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 "Rsync blocked — $REMOTE_SERVER_NAME is on the partnership blocklist"
|
||||||
error "Re-onboard the partnership to restore access: partnership_manager.sh --onboard"
|
error "Re-onboard the partnership to restore access: partnership_manager.sh --onboard"
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ acquire_lock
|
|||||||
|
|
||||||
detect_hosts
|
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 "Identity: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||||
log "Ramdisk: $RAMDISK_PATH"
|
log "Ramdisk: $RAMDISK_PATH"
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ fi
|
|||||||
# ━━━ Initialise State File ━━━
|
# ━━━ Initialise State File ━━━
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
if [[ "$DRY_RUN" == false ]]; then
|
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)
|
NOW=$(date +%s)
|
||||||
cat > "$STATE_FILE" <<EOF
|
cat > "$STATE_FILE" <<EOF
|
||||||
current_target=$RAMDISK_PATH
|
current_target=$RAMDISK_PATH
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ source "$SCRIPT_DIR/../load_config.sh"
|
|||||||
|
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
|
|
||||||
STATE_FILE="/tmp/transcode_state.db"
|
STATE_FILE="${TRANSCODE_STATE_FILE:-${STATE_DIR:-/tmp}/transcode_state.db}"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ━━━ Setup ━━━
|
# ━━━ Setup ━━━
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ done
|
|||||||
parse_args "${FILTERED_ARGS[@]}"
|
parse_args "${FILTERED_ARGS[@]}"
|
||||||
|
|
||||||
DOCKER_TIMEOUT=15
|
DOCKER_TIMEOUT=15
|
||||||
STATE_FILE="/tmp/transcode_state.db"
|
STATE_FILE="${TRANSCODE_STATE_FILE:-${STATE_DIR:-/tmp}/transcode_state.db}"
|
||||||
|
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# ━━━ Setup ━━━
|
# ━━━ Setup ━━━
|
||||||
@@ -250,7 +250,7 @@ get_ssd_free_gb() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get_flip_count() {
|
get_flip_count() {
|
||||||
local state_file="/tmp/transcode_flip_state.db"
|
local state_file="${STATE_DIR:-/tmp}/transcode_flip_state.db"
|
||||||
local current_hour
|
local current_hour
|
||||||
current_hour=$(date '+%Y-%m-%d-%H')
|
current_hour=$(date '+%Y-%m-%d-%H')
|
||||||
[[ ! -f "$state_file" ]] && echo "0" && return
|
[[ ! -f "$state_file" ]] && echo "0" && return
|
||||||
@@ -261,7 +261,7 @@ get_flip_count() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
increment_flip_count() {
|
increment_flip_count() {
|
||||||
local state_file="/tmp/transcode_flip_state.db"
|
local state_file="${STATE_DIR:-/tmp}/transcode_flip_state.db"
|
||||||
local current_hour
|
local current_hour
|
||||||
current_hour=$(date '+%Y-%m-%d-%H')
|
current_hour=$(date '+%Y-%m-%d-%H')
|
||||||
local current_count
|
local current_count
|
||||||
|
|||||||
@@ -1246,11 +1246,14 @@ get_rsync_opts() {
|
|||||||
|
|
||||||
LOCK_DIR="/tmp/unraid_locks"
|
LOCK_DIR="/tmp/unraid_locks"
|
||||||
|
|
||||||
# Ensure DATA_DIR exists — created here so every script that sources common.sh
|
# Ensure DATA_DIR and STATE_DIR exist — created here so every script that sources common.sh
|
||||||
# can safely write to it without checking first
|
# can safely write to either without checking first.
|
||||||
if [[ -n "${DATA_DIR:-}" ]] && [[ ! -d "$DATA_DIR" ]]; then
|
if [[ -n "${DATA_DIR:-}" ]] && [[ ! -d "$DATA_DIR" ]]; then
|
||||||
mkdir -p "$DATA_DIR" 2>/dev/null || true
|
mkdir -p "$DATA_DIR" 2>/dev/null || true
|
||||||
fi
|
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_COUNT_FILE="$LOCK_DIR/rsync_active_count"
|
||||||
RSYNC_MAX_CONCURRENT=3
|
RSYNC_MAX_CONCURRENT=3
|
||||||
|
|||||||
Reference in New Issue
Block a user