Auth stack certs tab, arrs db fallbacks, cert monitor cache, conf parser fix

- Auth stack: fold cert monitor into Auth Stack page as fourth tab (Certs);
  remove standalone cert page and top-level tab
- cert_monitor.sh: write JSON status cache to State_Files/cert_status.json
  after each run; expose per-domain days/expiry via _CERT_DAYS/_CERT_EXPIRY globals
- api/cert.php: new — serves cached cert status; falls back to configured
  domains as UNKN when no cache exists; POST action=run triggers live check
- arrs db fallbacks: vv_arr_cleanup_stats/discovery_stats/recovery_stats now
  read from data/*.db files when log JSON files don't yet exist
- config.php vv_conf_vars(): unescape bash \$ → $ so passwords with dollar
  signs read correctly from conf files
- host1.conf: fill in HOST1_NPM_USER/PASS and HOST1_LLDAP_USER/PASS
- Partnership adapter pattern: Unraid-specific container logic extracted to
  Plugin/unraid/Partnership/; platform-agnostic structure stays in Partnership/
- First-run wizard: uniform multi-step flow for all hosts; HOST2 pull moved
  to checklist; auto SSH keygen and API key creation on save
- api/checklist.php: live setup checklist with pull_master action
- Fullscreen toggle: hide Unraid header/menu; state persists via localStorage
This commit is contained in:
Gmer4Lfe
2026-06-05 23:17:30 -04:00
parent 17012bcd8c
commit 9c3ace95a7
43 changed files with 1605 additions and 1167 deletions
+56
View File
@@ -0,0 +1,56 @@
<?php
// API cache writer — runs every minute via Varaverk scheduler.
// Builds monitor + arrs payloads and writes them to /tmp/vv_cache/ so page
// loads can serve instantly from the file instead of making live HTTP calls.
//
// Called by api_cache_writer.sh (bash wrapper required by the scheduler).
$_base = dirname(__DIR__);
require_once $_base . '/include/monitor.php';
require_once $_base . '/include/vms.php';
require_once $_base . '/include/docker_folders.php';
require_once $_base . '/include/arrs.php';
$t = microtime(true);
// ── Monitor payload ───────────────────────────────────────────────────────────
// Call vv_api_data() once — result is static-cached for the rest of this process.
vv_api_data();
$monitor = [
'system' => vv_system_info(),
'fallback' => vv_fallback_state(),
'fallback_active' => vv_fallback_active(),
'partner' => vv_partner_state(),
'resources' => vv_system_resources(),
'cpu' => vv_cpu_per_core(),
'mem' => vv_memory_breakdown(),
'net' => vv_network_stats(),
'gpu' => vv_gpu_stats(),
'gpu_procs' => vv_gpu_processes(),
'containers' => vv_docker_containers(),
'stopped' => vv_docker_stopped(),
'transcode' => vv_transcode_sessions(),
'ups' => vv_ups_stats(),
'parity' => vv_parity_status(),
'storage' => vv_storage_pools(),
'array_disks' => vv_array_disks(),
'disk_io' => vv_disk_io_rates(),
'watchdog' => vv_watchdog_summary(),
'scripts' => vv_scripts_status(),
'rsync' => vv_rsync_status(),
'thresholds' => vv_disk_thresholds(),
'vms' => vv_get_vms(),
'docker_folders' => vv_get_docker_folders(),
'remote_hosts' => vv_remote_hosts_stats(),
'_api_status' => vv_api_get_status(),
'ts' => time(),
];
vv_cache_write('monitor', $monitor);
// ── Arrs payload ──────────────────────────────────────────────────────────────
$arrs = vv_arrs_all();
vv_cache_write('arrs', $arrs);
$elapsed = round((microtime(true) - $t) * 1000);
echo "Cache written in {$elapsed}ms — monitor + arrs\n";
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
# ==============================================================================================
# ================================= API Cache Writer ===========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Builds the monitor and arrs API payloads and writes them to /tmp/vv_cache/
# so page loads can serve from the file instantly instead of making live HTTP
# calls on every request.
#
# Runs every minute via the Varaverk scheduler. /tmp is tmpfs — files are
# RAM-speed reads and auto-cleared on reboot.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
php "$SCRIPT_DIR/api_cache_writer.php"
+301
View File
@@ -0,0 +1,301 @@
#!/bin/bash
# ==============================================================================================
# ============================= Conf Auto-Populate =============================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Reads credentials and settings from locally running services and writes
# them into the local host conf. Safe to run multiple times — only populates
# EMPTY fields, never overwrites existing values unless --overwrite is passed.
#
# After populating, pushes the updated conf to all partners via conf_sync.sh
# so they have the fresh keys in their /tmp/.vv/ cache immediately.
#
# ==============================================================================================
# AUTO-DETECTED FIELDS
# ==============================================================================================
#
# HOSTN_OWNER from hostname (strip unRAID- prefix, lowercase)
# HOSTN_RADARR_API_KEY from Radarr config.xml (found via docker volume mount)
# HOSTN_RADARR_URL from Radarr config.xml port
# HOSTN_SONARR_API_KEY from Sonarr config.xml
# HOSTN_SONARR_URL from Sonarr config.xml port
# HOSTN_LIDARR_API_KEY from Lidarr config.xml
# HOSTN_LIDARR_URL from Lidarr config.xml port
# HOSTN_RADARR_MOVIE_ROOT from Radarr rootFolder API
# HOSTN_SONARR_TV_ROOT from Sonarr rootFolder API
# HOSTN_LIDARR_MUSIC_ROOT from Lidarr rootFolder API
# HOSTN_SABNZBD_API_KEY from sabnzbd.ini
# HOSTN_SABNZBD_URL from sabnzbd.ini port
# HOSTN_SLSKD_API_KEY from slskd config.yml
# HOSTN_SLSKD_URL from slskd config.yml port
# HOSTN_QBIT_URL from qBittorrent.conf WebUI port
# HOSTN_QBIT_USERNAME from qBittorrent.conf WebUI username
# HOSTN_QBIT_PASSWORD from qBittorrent.conf WebUI password (plaintext only)
# HOSTN_EMBY_CONTAINER fuzzy match from docker ps
# HOSTN_EMBY_URL from docker port binding
# HOSTN_JELLYFIN_CONTAINER fuzzy match from docker ps
# HOSTN_JELLYFIN_URL from docker port binding
# HOSTN_TRANSCODE_SSD from Emby/Jellyfin container /transcode volume mount
# HOSTN_SYS_WATCHDOG_NIC from ip route default gateway interface
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# conf_populate.sh Populate empty fields only
# conf_populate.sh --overwrite Overwrite all detected fields (re-sync after arr key rotation)
# conf_populate.sh --dry-run Show what would be written, no changes
# conf_populate.sh --log Verbose output
# conf_populate.sh --no-push Skip pushing to partners after update
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../load_config.sh"
SCRIPTS_ROOT="$SCRIPTS_DIR"
OVERWRITE=false
NO_PUSH=false
FILTERED_ARGS=()
for arg in "$@"; do
case "$arg" in
--overwrite) OVERWRITE=true ;;
--no-push) NO_PUSH=true ;;
*) FILTERED_ARGS+=("$arg") ;;
esac
done
parse_args "${FILTERED_ARGS[@]}"
detect_hosts
[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
CONF_FILE="$SCRIPTS_ROOT/Configurations/${MY_ID,,}.conf"
[[ ! -f "$CONF_FILE" ]] && { error "Conf file not found: $CONF_FILE"; exit 1; }
log "$ICON_GEAR Config: conf=${CONF_FILE} overwrite=${OVERWRITE:-false} no-push=${NO_PUSH:-false}"
UPDATED=0
SKIPPED=0
echo ""
echo "━━━ $ICON_GEAR Conf Auto-Populate — $MY_ID ($LOCAL_SERVER_NAME) ━━━"
echo ""
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
[[ "$OVERWRITE" == true ]] && warn "OVERWRITE mode — existing values will be replaced"
# ── Helper: write a var into conf if empty (or --overwrite) ──────────────────
_set_conf_var() {
local var_name="$1" value="$2" label="$3"
[[ -z "$value" ]] && return
local current
current=$(grep -oP "(?<=^\s*${var_name}=\")[^\"]*" "$CONF_FILE" 2>/dev/null | head -1)
if [[ -n "$current" ]] && [[ "$OVERWRITE" == false ]]; then
log "$label: already set (${current:0:8}…) — skipping"
(( SKIPPED++ ))
return
fi
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would set $var_name = ${value:0:8}"
return
fi
if grep -q "^\s*${var_name}=" "$CONF_FILE"; then
sed -i "s|^\(\s*${var_name}\s*=\s*\)\"[^\"]*\"|\1\"${value}\"|" "$CONF_FILE"
else
printf '\n %s="%s"\n' "$var_name" "$value" >> "$CONF_FILE"
fi
info "$label: set ✅"
(( UPDATED++ ))
}
# ── Helper: find arr config dir via docker volume mount ───────────────────────
_arr_config_dir() {
local pattern="$1"
local container_name
container_name=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${pattern}")
[[ -z "$container_name" ]] && return 1
local config_path
config_path=$(docker inspect "$container_name" 2>/dev/null | \
jq -r '.[0].Mounts[]? | select(.Destination == "/config") | .Source' 2>/dev/null | head -1)
[[ -z "$config_path" ]] && config_path="${DOCKER_APPDATA_BASE:-/mnt/user/appdata}/${container_name}"
[[ -d "$config_path" ]] && echo "$config_path" || return 1
}
# ── Helper: read XML tag value ────────────────────────────────────────────────
_xml_val() {
local file="$1" tag="$2"
grep -oP "(?<=<${tag}>)[^<]+" "$file" 2>/dev/null | head -1
}
# ── Helper: get host-side port for a container's internal port ────────────────
_docker_host_port() {
local container="$1" container_port="$2"
docker inspect "$container" 2>/dev/null | \
jq -r --arg p "${container_port}/tcp" \
'.[0].NetworkSettings.Ports[$p]?[0].HostPort // empty' 2>/dev/null | head -1
}
# ── Helper: get host path for a container destination mount ──────────────────
_docker_volume_host() {
local container="$1" dest="$2"
docker inspect "$container" 2>/dev/null | \
jq -r --arg d "$dest" \
'.[0].Mounts[]? | select(.Destination == $d) | .Source' 2>/dev/null | head -1
}
# ==============================================================================================
# ── Owner short name ──────────────────────────────────────────────────────────────────────────
# ==============================================================================================
owner=$(echo "$LOCAL_SERVER_NAME" | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//i' | tr '[:upper:]' '[:lower:]')
_set_conf_var "${MY_ID}_OWNER" "$owner" "Owner short name"
# ==============================================================================================
# ── Arr API keys + URLs + root paths ─────────────────────────────────────────────────────────
# ==============================================================================================
for arr in radarr sonarr lidarr; do
arr_upper="${arr^^}"
config_dir=$(_arr_config_dir "$arr") || {
log "${arr_upper}: no running container found — skipping"
continue
}
config_xml="${config_dir}/config.xml"
if [[ ! -f "$config_xml" ]]; then
log "${arr_upper}: config.xml not found at $config_xml — skipping"
continue
fi
key=$(_xml_val "$config_xml" "ApiKey")
port=$(_xml_val "$config_xml" "Port")
port="${port:-$(case $arr in radarr) echo 7878;; sonarr) echo 8989;; lidarr) echo 8686;; esac)}"
url_base="http://localhost:${port}"
_set_conf_var "${MY_ID}_${arr_upper}_API_KEY" "$key" "${arr_upper} API key"
_set_conf_var "${MY_ID}_${arr_upper}_URL" "$url_base" "${arr_upper} URL"
if [[ -n "$key" ]]; then
case "$arr" in lidarr) api_ver="v1" ;; *) api_ver="v3" ;; esac
root_json=$(curl -sf --max-time 5 \
-H "X-Api-Key: $key" "${url_base}/api/${api_ver}/rootfolder" 2>/dev/null)
root_path=$(echo "$root_json" | jq -r '.[0].path // empty' 2>/dev/null)
case "$arr" in
radarr) _set_conf_var "${MY_ID}_RADARR_MOVIE_ROOT" "$root_path" "Radarr movie root" ;;
sonarr) _set_conf_var "${MY_ID}_SONARR_TV_ROOT" "$root_path" "Sonarr TV root" ;;
lidarr) _set_conf_var "${MY_ID}_LIDARR_MUSIC_ROOT" "$root_path" "Lidarr music root" ;;
esac
fi
done
# ==============================================================================================
# ── SABnzbd API key + URL ─────────────────────────────────────────────────────────────────────
# ==============================================================================================
sab_dir=$(_arr_config_dir "sabnzbd") && {
sab_ini=$(find "$sab_dir" -maxdepth 2 -name "sabnzbd.ini" 2>/dev/null | head -1)
if [[ -f "$sab_ini" ]]; then
sab_key=$(grep -oP '(?<=^api_key\s*=\s*)\S+' "$sab_ini" 2>/dev/null | head -1)
sab_port=$(grep -oP '(?<=^port\s*=\s*)\d+' "$sab_ini" 2>/dev/null | head -1)
_set_conf_var "${MY_ID}_SABNZBD_API_KEY" "$sab_key" "SABnzbd API key"
_set_conf_var "${MY_ID}_SABNZBD_URL" "http://localhost:${sab_port:-8080}" "SABnzbd URL"
fi
}
# ==============================================================================================
# ── slskd API key + URL ───────────────────────────────────────────────────────────────────────
# ==============================================================================================
slskd_dir=$(_arr_config_dir "slskd") && {
slskd_yml=$(find "$slskd_dir" -maxdepth 2 \( -name "*.yml" -o -name "*.yaml" \) 2>/dev/null | head -1)
if [[ -f "$slskd_yml" ]]; then
slskd_key=$(grep -oP '(?<=api_key:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
[[ -z "$slskd_key" ]] && \
slskd_key=$(grep -oP '(?<=apikey:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
slskd_port=$(grep -oP '(?<=port:\s)\d+' "$slskd_yml" 2>/dev/null | head -1)
_set_conf_var "${MY_ID}_SLSKD_API_KEY" "$slskd_key" "slskd API key"
_set_conf_var "${MY_ID}_SLSKD_URL" "http://localhost:${slskd_port:-5030}" "slskd URL"
fi
}
# ==============================================================================================
# ── qBittorrent URL + credentials ────────────────────────────────────────────────────────────
# ==============================================================================================
qbit_dir=$(_arr_config_dir "qbittorrent") && {
qbit_conf=$(find "$qbit_dir" -maxdepth 3 -name "qBittorrent.conf" 2>/dev/null | head -1)
if [[ -f "$qbit_conf" ]]; then
qbit_port=$(grep -oP '(?<=WebUI\\Port=)\d+' "$qbit_conf" 2>/dev/null | head -1)
qbit_user=$(grep -oP '(?<=WebUI\\Username=)\S+' "$qbit_conf" 2>/dev/null | head -1)
# Only capture plaintext password — PBKDF2 hashes are not usable
qbit_pass=$(grep -oP '(?<=WebUI\\Password=)[^\r\n]+' "$qbit_conf" 2>/dev/null | \
grep -v '@ByteArray' | head -1)
_set_conf_var "${MY_ID}_QBIT_URL" "http://localhost:${qbit_port:-8080}" "qBittorrent URL"
_set_conf_var "${MY_ID}_QBIT_USERNAME" "$qbit_user" "qBittorrent username"
_set_conf_var "${MY_ID}_QBIT_PASSWORD" "$qbit_pass" "qBittorrent password"
fi
}
# ==============================================================================================
# ── Media server container names + URLs + transcode path ─────────────────────────────────────
# ==============================================================================================
transcode_dir=""
for pattern in "emby" "jellyfin"; do
container=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${pattern}")
[[ -z "$container" ]] && continue
case "$pattern" in
emby)
host_port=$(_docker_host_port "$container" "8096")
_set_conf_var "${MY_ID}_EMBY_CONTAINER" "$container" "Emby container name"
_set_conf_var "${MY_ID}_EMBY_URL" "http://localhost:${host_port:-8096}" "Emby URL"
;;
jellyfin)
host_port=$(_docker_host_port "$container" "8096")
_set_conf_var "${MY_ID}_JELLYFIN_CONTAINER" "$container" "Jellyfin container name"
_set_conf_var "${MY_ID}_JELLYFIN_URL" "http://localhost:${host_port:-8095}" "Jellyfin URL"
;;
esac
# Transcode path: first container with a /transcode mount wins
if [[ -z "$transcode_dir" ]]; then
transcode_dir=$(_docker_volume_host "$container" "/transcode")
fi
done
[[ -n "$transcode_dir" ]] && \
_set_conf_var "${MY_ID}_TRANSCODE_SSD" "${transcode_dir%/}/" "Transcode SSD path"
# ==============================================================================================
# ── Network interface ─────────────────────────────────────────────────────────────────────────
# ==============================================================================================
nic=$(ip route show default 2>/dev/null | grep -oP '(?<=dev )\S+' | head -1)
_set_conf_var "${MY_ID}_SYS_WATCHDOG_NIC" "$nic" "Default NIC"
# ==============================================================================================
# ── Summary + push ────────────────────────────────────────────────────────────────────────────
# ==============================================================================================
echo ""
echo "━━━━━ $ICON_SUMMARY Conf Populate Summary ━━━━━"
echo " Updated: $UPDATED field(s)"
echo " Skipped: $SKIPPED already set"
echo " Conf: $CONF_FILE"
echo "━━━━━━━━━━━━━━━━━━━━━"
if [[ "$UPDATED" -gt 0 ]] && [[ "$DRY_RUN" == false ]] && [[ "$NO_PUSH" == false ]]; then
echo ""
info "Pushing updated conf to partners..."
bash "$SCRIPTS_ROOT/System_Essentials/conf_sync.sh" --push-only "${EXTRA_FLAGS[@]}" || true
fi
+299
View File
@@ -0,0 +1,299 @@
#!/bin/bash
# ==============================================================================================
# ============================= Recreate Shares ================================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Creates share directories on the correct disks after a fresh unRAID install
# or disk rebuild. Reads all .cfg files from /boot/config/shares/ and creates
# the corresponding directories on each disk listed in the shareInclude setting.
# The array must be started before running — /mnt/user must be mounted.
#
# Typically run on HOST2 after a full disk replacement or fresh install where
# share folders were lost but /boot/config/shares/*.cfg files were restored.
#
# ==============================================================================================
# OPERATIONAL MODEL
# ==============================================================================================
#
# For each share .cfg file:
# 1. Reads shareInclude= to determine which disks own this share
# 2. Creates /mnt/diskN/ShareName/ on each listed disk if it doesn't exist
# 3. Places a .recovery marker file in /mnt/user/ShareName/ via the union filesystem
#
# .recovery Marker
# Signals to rsync.sh that this is a fresh share with no existing data.
# rsync.sh checks for .recovery before running with --delete:
# .recovery present → rsync WITHOUT --delete (new files only, nothing removed)
# .recovery absent → rsync WITH --delete (normal mirror mode)
#
# Self-cleaning: after the first successful rsync the source side has no .recovery
# file, so the second nightly run deletes it from the mirror, restoring normal
# --delete behaviour automatically. No manual cleanup needed.
#
# ==============================================================================================
# OPERATIONAL SAFEGUARDS
# ==============================================================================================
#
# Single Instance Lock
# acquire_lock prevents duplicate runs placing duplicate .recovery markers.
#
# Root Required
# mkdir on /mnt/diskN requires root.
#
# Array Mount Check
# Exits cleanly if the array is not started — /mnt/user not mounted means
# all share operations would fail silently.
#
# Per-Disk Guards
# Missing disks are skipped with a warning and the rest continue — a single
# offline disk does not abort the full run.
#
# Empty Config Guard
# Warns if no share .cfg files are found — catches the case where
# /boot/config/shares/ was not restored.
#
# Notification Validated
# platform_require_cmd confirms the notify script is present before use.
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# recreate_shares.sh
# Read all .cfg files, create share directories, place .recovery markers.
#
# recreate_shares.sh --dry-run
# Show what directories and markers would be created. No changes.
#
# recreate_shares.sh --log
# Verbose output per share and per disk.
#
# recreate_shares.sh --status
# Show which shares exist in config and which directories exist on disk.
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../load_config.sh"
parse_args "$@"
SHARE_CFG_DIR="/boot/config/shares"
MARKER_FILE=".recovery"
CREATED=()
SKIPPED=()
FAILED=()
# ==============================================================================================
# ━━━ Setup ━━━
# ==============================================================================================
if [[ "$EUID" -ne 0 ]]; then
error "Must be run as root — mkdir on /mnt/diskN requires root"
exit 1
fi
platform_require_cmd \
"/usr/local/emhttp/plugins/dynamix/scripts/notify" \
"" "" \
"unRAID notify script" || warn "unRAID notify script not found — native notifications disabled"
acquire_lock
# detect_hosts() sets MY_ID — used in summary
detect_hosts
log "$ICON_GEAR Config: cfg-dir=${SHARE_CFG_DIR} marker=${MARKER_FILE}"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no directories or markers will be created"
# ==============================================================================================
# ━━━ Status ━━━
# ==============================================================================================
if [[ "$SHOW_STATUS" == true ]]; then
echo ""
echo "━━━━━ $ICON_SUMMARY STATUS ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo "$ICON_DISK Cfg dir: $SHARE_CFG_DIR"
echo "$ICON_DISK Marker: $MARKER_FILE"
echo ""
if ! mountpoint -q /mnt/user; then
warn "Array: NOT STARTED — /mnt/user not mounted"
else
echo " Array: started ✅"
fi
echo ""
echo "━━━ Share Config Files ━━━"
CFG_COUNT=0
for cfg in "$SHARE_CFG_DIR"/*.cfg; do
[[ ! -f "$cfg" ]] && continue
(( CFG_COUNT++ ))
SHARE_NAME=$(basename "$cfg" .cfg)
INCLUDE=$(grep '^shareInclude=' "$cfg" 2>/dev/null | cut -d'"' -f2)
MARKER_EXISTS="no"
[[ -f "/mnt/user/${SHARE_NAME}/${MARKER_FILE}" ]] && MARKER_EXISTS="yes"
echo " $ICON_DISK $SHARE_NAME — disks: ${INCLUDE:-none} — recovery marker: $MARKER_EXISTS"
done
[[ "$CFG_COUNT" -eq 0 ]] && warn "No .cfg files found in $SHARE_CFG_DIR"
echo "━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
# ==============================================================================================
# ━━━ Pre-flight ━━━
# ==============================================================================================
# Array must be started — /mnt/user must be mounted
if ! mountpoint -q /mnt/user; then
error "Array is not started — /mnt/user is not mounted"
warn "Start the array in the unRAID UI before running this script"
notify "Recreate shares failed on $(hostname) — array is not started" \
"Recreate Shares" "warning"
exit 1
fi
log "Array is started — /mnt/user is mounted ✅"
# Check share cfg directory exists and has files
if [[ ! -d "$SHARE_CFG_DIR" ]]; then
error "Share config directory not found: $SHARE_CFG_DIR"
error "Is /boot mounted? Is this the correct server?"
exit 1
fi
CFG_FILES=("$SHARE_CFG_DIR"/*.cfg)
if [[ ! -f "${CFG_FILES[0]}" ]]; then
warn "No share .cfg files found in $SHARE_CFG_DIR"
warn "Nothing to recreate — are share configs present on /boot?"
exit 0
fi
log "Found ${#CFG_FILES[@]} share .cfg file(s) in $SHARE_CFG_DIR"
# ==============================================================================================
# ━━━ Recreate Shares ━━━
# ==============================================================================================
echo ""
echo "━━━ $ICON_DISK Recreate Shares — $MY_ID ━━━"
echo ""
for cfg in "${CFG_FILES[@]}"; do
[[ ! -f "$cfg" ]] && continue
SHARE_NAME=$(basename "$cfg" .cfg)
INCLUDE=$(grep '^shareInclude=' "$cfg" 2>/dev/null | cut -d'"' -f2)
echo "━━━ $ICON_DISK $SHARE_NAME ━━━"
if [[ -z "$INCLUDE" ]]; then
warn "$SHARE_NAME — no shareInclude in .cfg — skipping"
SKIPPED+=("$SHARE_NAME")
echo ""
continue
fi
log "$SHARE_NAME — disks: $INCLUDE"
SHARE_OK=true
DIRS_CREATED=0
DIRS_EXISTED=0
# Create directory on each listed disk
IFS=',' read -ra DISKS <<< "$INCLUDE"
for disk in "${DISKS[@]}"; do
disk="${disk// /}" # trim whitespace
[[ -z "$disk" ]] && continue
DISK_MOUNT="/mnt/${disk}"
DISK_PATH="${DISK_MOUNT}/${SHARE_NAME}"
# Verify disk is mounted
if ! mountpoint -q "$DISK_MOUNT" 2>/dev/null; then
warn "$disk not mounted — skipping $DISK_PATH"
continue
fi
if [[ -d "$DISK_PATH" ]]; then
log "$disk/$SHARE_NAME already exists — skipping"
(( DIRS_EXISTED++ ))
else
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would create: $DISK_PATH"
(( DIRS_CREATED++ ))
elif mkdir -p "$DISK_PATH"; then
log "Created: $DISK_PATH"
(( DIRS_CREATED++ ))
else
error "Failed to create: $DISK_PATH"
SHARE_OK=false
fi
fi
done
# Place .recovery marker via /mnt/user (union filesystem)
MARKER_PATH="/mnt/user/${SHARE_NAME}/${MARKER_FILE}"
if [[ "$SHARE_OK" == true ]]; then
if [[ -f "$MARKER_PATH" ]]; then
log ".recovery marker already exists in $SHARE_NAME"
CREATED+=("$SHARE_NAME")
elif [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — would place marker: $MARKER_PATH"
CREATED+=("$SHARE_NAME")
elif touch "$MARKER_PATH" 2>/dev/null; then
log "Marker placed: $MARKER_PATH"
CREATED+=("$SHARE_NAME")
else
warn "$SHARE_NAME — could not place .recovery marker"
warn "Share directory may not be visible via /mnt/user yet"
warn "Try: touch /mnt/user/${SHARE_NAME}/.recovery manually after verifying share"
SKIPPED+=("$SHARE_NAME")
fi
else
FAILED+=("$SHARE_NAME")
fi
[[ "$DIRS_CREATED" -gt 0 ]] && warn "$SHARE_NAME — created $DIRS_CREATED dir(s) on disk"
[[ "$DIRS_EXISTED" -gt 0 ]] && log "$SHARE_NAME$DIRS_EXISTED dir(s) already existed"
echo ""
done
# ==============================================================================================
# ━━━ Summary ━━━
# ==============================================================================================
echo "━━━━━ $ICON_SUMMARY RECREATE SHARES SUMMARY ━━━━━"
echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)"
echo ""
[[ ${#CREATED[@]} -gt 0 ]] && warn "Created + marked: ${CREATED[*]}"
[[ ${#SKIPPED[@]} -gt 0 ]] && warn "Skipped: ${SKIPPED[*]}"
[[ ${#FAILED[@]} -gt 0 ]] && echo "$ICON_ERROR Failed: ${FAILED[*]}"
echo ""
echo " Created: ${#CREATED[@]}"
echo " Skipped: ${#SKIPPED[@]}"
echo " Failed: ${#FAILED[@]}"
if [[ "$DRY_RUN" == false && ${#CREATED[@]} -gt 0 ]]; then
echo ""
echo "━━━ Next Steps ━━━"
echo " 1. $ICON_HEALTH Verify shares are visible in unRAID UI"
echo " 2. $ICON_SYNC Run initial rsync push from HOST1 → HOST2"
echo " rsync.sh will detect .recovery markers and skip --delete"
echo " Normal --delete mode restores automatically on second nightly run"
echo " 3. $ICON_GEAR No manual config changes needed — markers self-clean ✅"
fi
echo ""
if [[ "$DRY_RUN" == true ]]; then
warn "DRY RUN — no changes made"
elif [[ ${#FAILED[@]} -gt 0 ]]; then
echo "$ICON_ERROR Status: completed with failures"
notify "Recreate shares failed on $(hostname) ($MY_ID) — failed: ${FAILED[*]}" \
"Recreate Shares" "warning"
exit 1
else
echo "$ICON_DONE Status: done — ${#CREATED[@]} created, ${#SKIPPED[@]} skipped"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+142
View File
@@ -0,0 +1,142 @@
#!/bin/bash
# ==============================================================================================
# ============================= Remote Arr Cache Writer ========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# SSHes to each remote partner host, calls vv_arrs_local_node() on their PHP
# stack, and caches the result locally in /tmp/vv_cache/arrs_remote_hostN.json.
#
# The arrs page reads these files for instant initial load without hitting the
# remote arr APIs on every page view. This script runs every 2 hours so remote
# library counts stay reasonably current without hammering the network.
#
# Accepts --host=HOST2 to refresh a single host (used by the UI refresh button).
#
# ==============================================================================================
# USAGE
# ==============================================================================================
#
# remote_arr_cache_writer.sh — refresh all remote hosts
# remote_arr_cache_writer.sh --host=HOST2 — refresh one host only
# remote_arr_cache_writer.sh --dry-run — show what would happen
# remote_arr_cache_writer.sh --log — verbose output
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../load_config.sh"
parse_args "$@"
acquire_lock
detect_hosts
# Parse --host= from raw args
TARGET_HOST=""
for arg in "$@"; do
case "$arg" in --host=*) TARGET_HOST="${arg#--host=}" ;; esac
done
mkdir -p "$VV_CACHE_DIR"
log "$ICON_GEAR Config: target=${TARGET_HOST:-all hosts} ssh-key=${SSH_KEY}"
FETCH_OK=0
FETCH_FAIL=0
FETCH_SKIP=0
echo ""
echo "━━━ $ICON_SYNC Remote Arr Cache Writer ━━━"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
[[ -n "$TARGET_HOST" ]] && echo " Target: $TARGET_HOST"
echo ""
for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do
[[ "$host_var" == "$MY_ID" ]] && continue
hostname="${!host_var:-}"
[[ -z "$hostname" ]] && continue
# If targeting a specific host, skip others
if [[ -n "$TARGET_HOST" ]]; then
[[ "${host_var,,}" != "${TARGET_HOST,,}" && "$host_var" != "$TARGET_HOST" ]] && continue
fi
host_id="${host_var,,}" # host1, host2, …
cache_file="$VV_CACHE_DIR/arrs_remote_${host_id}.json"
echo " $host_var ($hostname)…"
if [[ "$DRY_RUN" == true ]]; then
warn " DRY RUN — would SSH to $hostname and cache arr data"
(( FETCH_SKIP++ ))
continue
fi
# Resolve Tailscale IP
REMOTE_IP=$(resolve_tailscale_ip "$hostname")
if [[ -z "$REMOTE_IP" ]]; then
warn " $host_var: cannot resolve Tailscale IP for $hostname — skipping"
(( FETCH_FAIL++ ))
continue
fi
log " $host_var: resolved $hostname$REMOTE_IP"
# Single SSH connection — fetch arrs + monitor stats together
RESULT=$(ssh -i "$SSH_KEY" \
-o ConnectTimeout=10 \
-o StrictHostKeyChecking=no \
-o BatchMode=yes \
"root@${REMOTE_IP}" \
"php -r \"
require_once '/usr/local/emhttp/plugins/varaverk/include/arrs.php';
require_once '/usr/local/emhttp/plugins/varaverk/include/unraid_api.php';
echo json_encode([
'arrs' => vv_arrs_local_node(),
'monitor' => vv_local_host_stats(),
]);
\"" 2>/dev/null)
if [[ -z "$RESULT" ]]; then
warn " $host_var: empty SSH response — skipping"
(( FETCH_FAIL++ ))
continue
fi
# Validate combined JSON
if ! echo "$RESULT" | php -r "
\$d = json_decode(file_get_contents('php://stdin'), true);
exit(!is_array(\$d) || !isset(\$d['arrs'], \$d['monitor']) ? 1 : 0);
" 2>/dev/null; then
warn " $host_var: invalid JSON — skipping"
log " Response: ${RESULT:0:200}"
(( FETCH_FAIL++ ))
continue
fi
# Save arr cache
echo "$RESULT" | php -r "
\$d = json_decode(file_get_contents('php://stdin'), true);
file_put_contents('$cache_file', json_encode(\$d['arrs']));
" 2>/dev/null
# Save monitor cache
MONITOR_CACHE="$VV_CACHE_DIR/monitor_remote_${host_id}.json"
echo "$RESULT" | php -r "
\$d = json_decode(file_get_contents('php://stdin'), true);
file_put_contents('$MONITOR_CACHE', json_encode(\$d['monitor']));
" 2>/dev/null
CACHED_TYPES=$(echo "$RESULT" | php -r "
\$d = json_decode(file_get_contents('php://stdin'), true);
echo implode(', ', array_column(\$d['arrs']['arrs'] ?? [], 'type'));
" 2>/dev/null)
echo " $host_var: arrs [${CACHED_TYPES:-none}] + monitor stats cached ✅"
(( FETCH_OK++ ))
done
echo ""
echo "━━━━━ $ICON_SUMMARY Cache Writer Summary ━━━━━"
echo " $FETCH_OK updated · $FETCH_FAIL failed · $FETCH_SKIP skipped"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+329
View File
@@ -0,0 +1,329 @@
#!/bin/bash
# ==============================================================================================
# ================================= Storage Migration ==========================================
# ==============================================================================================
#
# PURPOSE
# ─────────────────────────────────────────────────────────────────────────────
# Migrates Varaverk between internal NVMe and USB flash storage modes.
#
# Internal mode: SCRIPTS_DIR = /boot/config/plugins/varaverk
# All scripts, conf, state, and git repo live on fast internal storage.
# Direct git pull/push. Zero write-wear concern.
#
# Flash mode: SCRIPTS_DIR = /mnt/user/appdata/Varaverk
# All scripts, conf, state, and git repo live in appdata.
# Preserves USB flash lifetime. Array must be started for Varaverk to function.
# git_pull_execute.sh syncs Plugin/ back to /boot/ after each pull so the
# Unraid webUI always serves current PHP files.
#
# What this script updates:
# varaverk.cfg SCRIPTS_DIR
# master.conf TARGET_DIR
# host*.conf HOST*_STORAGE_MODE_INTERNAL
# varaverk.cron rebuilt via PHP (job paths regenerated for new SCRIPTS_DIR)
#
# ==============================================================================================
# RUNTIME MODES
# ==============================================================================================
#
# storage_migrate.sh --to=internal
# Migrate to /boot/config/plugins/varaverk
#
# storage_migrate.sh --to=flash
# Migrate to /mnt/user/appdata/Varaverk
#
# storage_migrate.sh --dry-run --to=<mode>
# Show what would happen — no changes made
#
# storage_migrate.sh --status
# Show current mode, paths, and boot device info
#
# ==============================================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/../../../load_config.sh"
parse_args "$@"
acquire_lock
VV_CFG="/boot/config/plugins/varaverk/varaverk.cfg"
INTERNAL_DIR="/boot/config/plugins/varaverk"
FLASH_DIR="/mnt/user/appdata/Varaverk"
# ──────────────────────────────────────────────────────────────────────────────
# Parse --to= from raw args (parse_args doesn't handle this flag)
TO_MODE=""
for arg in "$@"; do
case "$arg" in
--to=internal) TO_MODE="internal" ;;
--to=flash) TO_MODE="flash" ;;
esac
done
# ──────────────────────────────────────────────────────────────────────────────
# Boot device detection
detect_boot_storage() {
local boot_part boot_disk transport
boot_part=$(findmnt -n -o SOURCE /boot 2>/dev/null)
boot_disk=$(lsblk -no pkname "$boot_part" 2>/dev/null)
transport=$(lsblk -dno TRAN "/dev/$boot_disk" 2>/dev/null | tr '[:upper:]' '[:lower:]')
echo "${transport:-unknown}"
}
# ──────────────────────────────────────────────────────────────────────────────
# Status
if [[ "$SHOW_STATUS" == true ]]; then
TRANSPORT=$(detect_boot_storage)
DETECTED=$([[ "$TRANSPORT" == "usb" ]] && echo "flash" || echo "internal")
echo ""
echo "━━━━━ $ICON_SUMMARY STORAGE STATUS ━━━━━"
echo "$ICON_GEAR SCRIPTS_DIR: $SCRIPTS_DIR"
echo "$ICON_GEAR varaverk.cfg: $VV_CFG"
echo "$ICON_HOST Boot device: transport=$TRANSPORT → detected=$DETECTED"
echo "$ICON_GEAR Target dirs:"
echo " internal: $INTERNAL_DIR"
echo " flash: $FLASH_DIR"
CONF_MODE=$(grep -m1 "${MY_ID}_STORAGE_MODE_INTERNAL" "$CONF_FILE" 2>/dev/null | cut -d= -f2 | tr -d '"' | tr -d '[:space:]')
echo "$ICON_GEAR conf setting: ${MY_ID}_STORAGE_MODE_INTERNAL=${CONF_MODE:-not set}"
if [[ "$SCRIPTS_DIR" == "$INTERNAL_DIR" ]]; then
echo "$ICON_DONE Current mode: INTERNAL ✅"
elif [[ "$SCRIPTS_DIR" == "$FLASH_DIR" ]]; then
echo "$ICON_DONE Current mode: FLASH ✅"
else
echo "$ICON_WARN Current mode: CUSTOM ($SCRIPTS_DIR)"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
exit 0
fi
# ──────────────────────────────────────────────────────────────────────────────
if [[ -z "$TO_MODE" ]]; then
error "Usage: storage_migrate.sh --to=internal|flash [--dry-run] [--log]"
exit 1
fi
SRC="$SCRIPTS_DIR"
DST=$([[ "$TO_MODE" == "internal" ]] && echo "$INTERNAL_DIR" || echo "$FLASH_DIR")
NEW_INTERNAL=$([[ "$TO_MODE" == "internal" ]] && echo "true" || echo "false")
log "$ICON_GEAR Config: to=${TO_MODE} src=${SRC} dst=${DST} dry-run=${DRY_RUN}"
echo ""
echo "━━━━━ $ICON_SYNC Storage Migration ━━━━━"
echo "$ICON_GEAR From: $SRC"
echo "$ICON_GEAR To: $DST"
echo "$ICON_GEAR Mode: $TO_MODE"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
echo ""
# ──────────────────────────────────────────────────────────────────────────────
# Pre-flight checks
if [[ "$SRC" == "$DST" ]]; then
info "Already in $TO_MODE mode — nothing to do"
exit 0
fi
if [[ "$TO_MODE" == "flash" ]]; then
if ! mountpoint -q /mnt/user 2>/dev/null; then
error "Array not started — /mnt/user is not mounted. Start the array before migrating to flash."
exit 1
fi
fi
if [[ ! -f "$SRC/load_config.sh" ]]; then
error "Source directory looks invalid: $SRC (load_config.sh not found)"
exit 1
fi
# ──────────────────────────────────────────────────────────────────────────────
# Step 1: Git push — ensure remote has everything before we touch the local repo
echo "━━━ $ICON_SYNC Step 1: Git push ━━━"
if [[ "$DRY_RUN" == false ]]; then
if [[ -d "$SRC/.git" ]]; then
log "Pushing to Gitea before migration..."
if GIT_SSH_COMMAND="ssh -i $GITEA_SSH_KEY -p $SSH_PORT" \
git -C "$SRC" push origin main 2>&1 | while IFS= read -r line; do echo " $line"; done; then
echo " Git push complete ✅"
else
warn "Git push failed — continuing (data safe locally, push manually after migration)"
fi
else
warn "No .git directory in $SRC — skipping push"
fi
else
warn "DRY RUN — would push $SRC to Gitea"
fi
# ──────────────────────────────────────────────────────────────────────────────
# Step 2: Rsync content to destination
echo ""
echo "━━━ $ICON_SYNC Step 2: Copy files ━━━"
if [[ "$DRY_RUN" == false ]]; then
mkdir -p "$DST"
echo " rsync: $SRC/ → $DST/"
if rsync -av --delete \
--exclude='.git' \
"$SRC/" "$DST/" 2>&1 | \
grep -v "/$" | \
while IFS= read -r line; do log "$line"; done; then
echo " Files copied ✅"
else
error "rsync failed — aborting migration"
exit 1
fi
# Copy .git separately (rsync --exclude='.git' above skipped it)
echo " Copying .git..."
if cp -a "$SRC/.git" "$DST/.git" 2>/dev/null || \
rsync -a "$SRC/.git/" "$DST/.git/" 2>/dev/null; then
echo " .git copied ✅"
else
error ".git copy failed — aborting"
exit 1
fi
# Mark git safe directory
git config --global --add safe.directory "$DST" 2>/dev/null
else
warn "DRY RUN — would rsync $SRC/ → $DST/ (including .git)"
fi
# ──────────────────────────────────────────────────────────────────────────────
# Step 3: Update varaverk.cfg
echo ""
echo "━━━ $ICON_GEAR Step 3: Update varaverk.cfg ━━━"
if [[ "$DRY_RUN" == false ]]; then
if grep -q '^SCRIPTS_DIR' "$VV_CFG"; then
sed -i "s|^SCRIPTS_DIR=.*|SCRIPTS_DIR=\"$DST\"|" "$VV_CFG"
else
echo "SCRIPTS_DIR=\"$DST\"" >> "$VV_CFG"
fi
echo " SCRIPTS_DIR → $DST"
else
warn "DRY RUN — would set SCRIPTS_DIR=\"$DST\" in $VV_CFG"
fi
# ──────────────────────────────────────────────────────────────────────────────
# Step 4: Update TARGET_DIR, DATA_DIR, STATE_DIR in master.conf (new location)
echo ""
echo "━━━ $ICON_GEAR Step 4: Update master.conf paths ━━━"
NEW_MASTER="$DST/Configurations/master.conf"
if [[ "$DRY_RUN" == false ]]; then
if [[ -f "$NEW_MASTER" ]]; then
sed -i "s|^\(\s*TARGET_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST\"|" "$NEW_MASTER"
sed -i "s|^\(\s*DATA_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST/data\"|" "$NEW_MASTER"
sed -i "s|^\(\s*STATE_DIR\s*=\s*\)\"[^\"]*\"|\1\"$DST/State_Files\"|" "$NEW_MASTER"
echo " TARGET_DIR → $DST"
echo " DATA_DIR → $DST/data ✅"
echo " STATE_DIR → $DST/State_Files ✅"
else
error "master.conf not found at $NEW_MASTER"
exit 1
fi
else
warn "DRY RUN — would update TARGET_DIR, DATA_DIR, STATE_DIR in master.conf"
fi
# ──────────────────────────────────────────────────────────────────────────────
# Step 5: Update STORAGE_MODE_INTERNAL in host*.conf (new location)
echo ""
echo "━━━ $ICON_GEAR Step 5: Update STORAGE_MODE_INTERNAL ━━━"
NEW_CONF="$DST/Configurations/${MY_ID,,}.conf"
if [[ "$DRY_RUN" == false ]]; then
if [[ -f "$NEW_CONF" ]]; then
if grep -q "${MY_ID}_STORAGE_MODE_INTERNAL" "$NEW_CONF"; then
sed -i "s|^\(\s*${MY_ID}_STORAGE_MODE_INTERNAL\s*=\s*\).*|\1${NEW_INTERNAL}|" "$NEW_CONF"
else
sed -i "/# ━━━ Storage mode/a\\ ${MY_ID}_STORAGE_MODE_INTERNAL=${NEW_INTERNAL}" "$NEW_CONF"
fi
echo " ${MY_ID}_STORAGE_MODE_INTERNAL → $NEW_INTERNAL"
else
warn "${MY_ID,,}.conf not found at $NEW_CONF — skipping conf update"
fi
else
warn "DRY RUN — would set ${MY_ID}_STORAGE_MODE_INTERNAL=$NEW_INTERNAL"
fi
# ──────────────────────────────────────────────────────────────────────────────
# Step 6: Rebuild cron (paths must reference new SCRIPTS_DIR)
echo ""
echo "━━━ $ICON_GEAR Step 6: Rebuild cron ━━━"
if [[ "$DRY_RUN" == false ]]; then
RESULT=$(php -r "
define('PLUGIN_CFG', '/boot/config/plugins/varaverk/varaverk.cfg');
\$_c = @parse_ini_file(PLUGIN_CFG) ?: [];
define('SCRIPTS_DIR', \$_c['SCRIPTS_DIR'] ?? '/boot/config/plugins/varaverk');
define('CONF_DIR', SCRIPTS_DIR . '/Configurations');
define('DATA_DIR', SCRIPTS_DIR . '/data');
define('STATE_DIR', SCRIPTS_DIR . '/State_Files');
define('LOG_DIR', '/var/log/varaverk');
define('VV_SETUP_STATE_FILE', STATE_DIR . '/varaverk_setup.db');
require_once '/usr/local/emhttp/plugins/varaverk/include/confform.php';
require_once '/usr/local/emhttp/plugins/varaverk/include/scheduler.php';
\$ok = vv_cron_rebuild(vv_schedule_load());
echo \$ok ? 'ok' : 'fail';
" 2>/dev/null)
if [[ "$RESULT" == "ok" ]]; then
echo " Cron rebuilt ✅"
else
warn "Cron rebuild failed — run Settings → Scheduler → Save to regenerate"
fi
else
warn "DRY RUN — would rebuild cron with new SCRIPTS_DIR paths"
fi
# ──────────────────────────────────────────────────────────────────────────────
# Step 7: Flash mode — sync Plugin/ to /boot/ so webUI is current
if [[ "$TO_MODE" == "flash" && "$DRY_RUN" == false ]]; then
echo ""
echo "━━━ $ICON_SYNC Step 7: Sync Plugin/ → /boot/ ━━━"
if rsync -a --delete "$DST/Plugin/" "$INTERNAL_DIR/Plugin/" 2>/dev/null; then
echo " Plugin/ synced to /boot/ ✅"
else
warn "Plugin/ sync to /boot/ failed — webUI may be stale"
fi
fi
# ──────────────────────────────────────────────────────────────────────────────
# Step 8: Delete old location
echo ""
echo "━━━ $ICON_GEAR Step $([[ "$TO_MODE" == "flash" ]] && echo 8 || echo 7): Clean up old location ━━━"
if [[ "$DRY_RUN" == false ]]; then
if [[ "$SRC" == "$INTERNAL_DIR" ]]; then
# Migrating internal→flash: keep varaverk.cfg and Plugin/ in /boot/, remove everything else
echo " Removing scripts/conf/state from /boot/ (keeping Plugin/ and varaverk.cfg)..."
find "$SRC" -mindepth 1 -maxdepth 1 \
! -name 'Plugin' \
! -name 'varaverk.cfg' \
! -name '*.plg' \
! -name '*.txz' \
-exec rm -rf {} + 2>/dev/null
echo " /boot/ cleaned ✅ (Plugin/ and varaverk.cfg preserved)"
else
# Migrating flash→internal: remove appdata copy entirely
echo " Removing $SRC..."
rm -rf "$SRC"
echo " $SRC removed ✅"
fi
else
if [[ "$SRC" == "$INTERNAL_DIR" ]]; then
warn "DRY RUN — would remove scripts/conf/state from /boot/ (keeping Plugin/ and varaverk.cfg)"
else
warn "DRY RUN — would remove $SRC"
fi
fi
# ──────────────────────────────────────────────────────────────────────────────
echo ""
echo "━━━━━ $ICON_DONE Migration complete ━━━━━"
echo "$ICON_GEAR Mode: $TO_MODE"
echo "$ICON_GEAR SCRIPTS_DIR: $DST"
if [[ "$TO_MODE" == "flash" ]]; then
echo ""
warn "IMPORTANT: Varaverk requires the array to be started to function in flash mode."
warn "The webUI plugin tab will load normally at all times (Plugin/ stays in /boot/)."
fi
echo ""
echo " Reload the Varaverk plugin tab to apply changes."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"