+ Varaverk stores all scripts, configuration, state files, and the git repository in
+ SCRIPTS_DIR.
+
+ Internal NVMe/SSD — direct access, zero write-wear concern, git pull/push from boot volume.
+ USB Flash — preserves flash lifetime. Requires array to be started. git pull syncs
+ Plugin/
+ back to /boot/
+ after each pull so the webUI always stays current.
+
+
+
+ Array must be started for Varaverk to function in flash mode. The plugin tab is always accessible.
+
+
+ Array is not started — cannot migrate to flash mode right now. Start the array first.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugin/unraid/run_job.sh b/Plugin/unraid/run_job.sh
index 351a400..c7c3faa 100755
--- a/Plugin/unraid/run_job.sh
+++ b/Plugin/unraid/run_job.sh
@@ -44,7 +44,9 @@ if [[ "$MANUAL" == false && -f "$MANUAL_TS_FILE" ]]; then
# Only suppress for interval-based crons (*/N * * * *).
# Static schedules like "30 2 * * 0" run at their appointed time and are never suppressed.
INTERVAL=0
- SCHEDULE_FILE="/boot/config/plugins/varaverk/schedule.json"
+ _VV_CFG="/boot/config/plugins/varaverk/varaverk.cfg"
+ _SCRIPTS_DIR=$(grep -m1 '^SCRIPTS_DIR' "$_VV_CFG" 2>/dev/null | cut -d= -f2 | tr -d '"'"'" | tr -d '[:space:]')
+ SCHEDULE_FILE="${_SCRIPTS_DIR:-/boot/config/plugins/varaverk}/schedule.json"
if [[ -f "$SCHEDULE_FILE" ]]; then
CRON_EXPR=$(php -r "
\$s = json_decode(file_get_contents('$SCHEDULE_FILE'), true) ?: [];
diff --git a/Tools/storage_migrate.sh b/Tools/storage_migrate.sh
new file mode 100755
index 0000000..cbb86f6
--- /dev/null
+++ b/Tools/storage_migrate.sh
@@ -0,0 +1,323 @@
+#!/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=
+# 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")
+
+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 in master.conf (new location)
+echo ""
+echo "━━━ $ICON_GEAR Step 4: Update TARGET_DIR ━━━"
+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"
+ echo " TARGET_DIR → $DST ✅"
+ else
+ error "master.conf not found at $NEW_MASTER"
+ exit 1
+ fi
+else
+ warn "DRY RUN — would set TARGET_DIR=\"$DST\" 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/" "/boot/config/plugins/varaverk/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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
diff --git a/git_pull_execute.sh b/git_pull_execute.sh
index eeb3956..2684cb0 100755
--- a/git_pull_execute.sh
+++ b/git_pull_execute.sh
@@ -255,6 +255,19 @@ else
log "Setting executable permissions on all .sh files..."
find "$TARGET_DIR" -type f -name "*.sh" -exec chmod +x {} \;
echo " Permissions set on .sh files"
+
+ # ── Flash mode: sync Plugin/ to /boot/ so the webUI picks up updates ─────
+ # In flash mode SCRIPTS_DIR is in appdata — Plugin/ lives in the repo there
+ # but Unraid serves PHP from /boot/. Sync after every pull to keep them in step.
+ if [[ "$TARGET_DIR" != "/boot/config/plugins/varaverk" ]]; then
+ echo ""
+ echo "━━━ $ICON_SYNC Flash mode: sync Plugin/ → /boot/ ━━━"
+ if rsync -a --delete "$TARGET_DIR/Plugin/" "/boot/config/plugins/varaverk/Plugin/" 2>/dev/null; then
+ echo " Plugin/ synced to /boot/ ✅"
+ else
+ warn "Plugin/ sync to /boot/ failed — webUI may be stale until next pull"
+ fi
+ fi
fi
END=$(date +%s)