grep -n "source.*load_config\|SCRIPT_DIR=" \
  /boot/config/plugins/varaverk/System_Essentials/unraid_api_key_renew.sh \
  /boot/config/plugins/varaverk/System_Essentials/mover_stop.sh \
  /boot/config/plugins/varaverk/System_Essentials/user_scripts_stop.sh \
  /boot/config/plugins/varaverk/System_Essentials/php_fpm_max_children.sh \
  /boot/config/plugins/varaverk/Watchdogs/System/webgui_watchdog.sh \
  /boot/config/plugins/varaverk/Tools/recreate_shares.sh \
  /boot/config/plugins/varaverk/Plugin/unraid/tools/conf_populate.sh \
  /boot/config/plugins/varaverk/Plugin/unraid/tools/storage_migrate.sh

Check current source paths in all affected scripts


/boot/config/plugins/varaverk/System_Essentials/user_scripts_stop.sh:71:SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
/boot/config/plugins/varaverk/System_Essentials/user_scripts_stop.sh:73:source "$SCRIPT_DIR/../load_config.sh"
/boot/config/plugins/varaverk/System_Essentials/mover_stop.sh:75:SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
/boot/config/plugins/varaverk/System_Essentials/mover_stop.sh:77:source "$SCRIPT_DIR/../load_config.sh"
/boot/config/plugins/varaverk/System_Essentials/unraid_api_key_renew.sh:31:SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
/boot/config/plugins/varaverk/System_Essentials/unraid_api_key_renew.sh:33:source "$SCRIPT_DIR/../load_config.sh"
/boot/config/plugins/varaverk/Plugin/unraid/tools/storage_migrate.sh:44:SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
/boot/config/plugins/varaverk/Plugin/unraid/tools/storage_migrate.sh:45:source "$SCRIPT_DIR/../../../load_config.sh"
/boot/config/plugins/varaverk/Plugin/unraid/tools/conf_populate.sh:43:SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
/boot/config/plugins/varaverk/Plugin/unraid/tools/conf_populate.sh:44:source "$SCRIPT_DIR/../../../load_config.sh"
/boot/config/plugins/varaverk/System_Essentials/php_fpm_max_children.sh:92:SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
/boot/config/plugins/varaverk/System_Essentials/php_fpm_max_children.sh:94:source "$SCRIPT_DIR/../load_config.sh"
/boot/config/plugins/varaverk/Tools/recreate_shares.sh:78:SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
/boot/config/plugins/varaverk/Tools/recreate_shares.sh:80:source "$SCRIPT_DIR/../load_config.sh"
/boot/config/plugins/varaverk/Watchdogs/System/webgui_watchdog.sh:93:SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
/boot/config/plugins/varaverk/Watchdogs/System/webgui_watchdog.sh:95:source "$SCRIPT_DIR/../../load_config.sh"


Bash

grep -n "source.*load_config\|SCRIPT_DIR=" /boot/config/plugins/varaverk/user_script_plug-in.sh | head -10
echo "---"
grep -n "user_script_plug-in\|user_script_plug" /boot/config/plugins/varaverk/Plugin/unraid/include/scheduler.php

Check user_script_plug-in.sh source path and scheduler reference


---
149:// Parse user_script_plug-in.sh into an array of script blocks.
152:    $file   = SCRIPTS_DIR . '/user_script_plug-in.sh';


grep -n "SCRIPTS_DIR\|load_config" /boot/config/plugins/varaverk/user_script_plug-in.sh | head -5

Check if user_script_plug-in.sh sources load_config


(Bash completed with no output)


grep -n "confform.php\|webgui_watchdog" /boot/config/plugins/varaverk/Plugin/unraid/include/confform.php | head -10

Check confform.php webgui_watchdog reference and context


4:// confform.php — script→conf-section mapping, field parsing, and write-back.
27:    'Watchdogs/System/webgui_watchdog.sh'             => ['WebGUI Watchdog'],


Good picture. Let me check the two `unraid_api_key_renew` references in master.conf for context:


grep -n "unraid_api_key_renew\|webgui_watchdog" /boot/config/plugins/varaverk/Configurations/master.conf

Check all master.conf references with context


284:        "System_Essentials/unraid_api_key_renew.sh"   # re-register Varaverk API key — registry is ephemeral
317:        "System_Essentials/unraid_api_key_renew.sh"   # re-register API key if registry lost it
326:        "Watchdogs/System/webgui_watchdog.sh"    # WebGUI availability — nginx → php-fpm → emhttp


- Orchestrators/array_started.sh / array_stopping.sh, stay, all others migrate with the same folder stucture... i want folders in plugins to match main. easy to follow


Clear picture. Here's the full plan:

- Move 7 scripts to `Plugin/unraid/{System_Essentials,Watchdogs/System,Tools}/`
- Rename `Plugin/unraid/tools/` → `Plugin/unraid/Tools/` (matches main `Tools/`)
- Fix source depths in all moved scripts
- Update master.conf × 2, confform.php, scheduler.php, arrs.php, storage.php, schedule.json

Starting with the file moves:


mkdir -p /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials \
           /boot/config/plugins/varaverk/Plugin/unraid/Watchdogs/System \
           /boot/config/plugins/varaverk/Plugin/unraid/Tools

# Move the 7 scripts
cp -p /boot/config/plugins/varaverk/System_Essentials/unraid_api_key_renew.sh  /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/
cp -p /boot/config/plugins/varaverk/System_Essentials/mover_stop.sh            /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/
cp -p /boot/config/plugins/varaverk/System_Essentials/user_scripts_stop.sh     /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/
cp -p /boot/config/plugins/varaverk/System_Essentials/php_fpm_max_children.sh  /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/
cp -p /boot/config/plugins/varaverk/Watchdogs/System/webgui_watchdog.sh        /boot/config/plugins/varaverk/Plugin/unraid/Watchdogs/System/
cp -p /boot/config/plugins/varaverk/Tools/recreate_shares.sh                   /boot/config/plugins/varaverk/Plugin/unraid/Tools/
cp -p /boot/config/plugins/varaverk/user_script_plug-in.sh                     /boot/config/plugins/varaverk/Plugin/unraid/

# Move existing tools/ contents into Tools/ (rename)
cp -p /boot/config/plugins/varaverk/Plugin/unraid/tools/* /boot/config/plugins/varaverk/Plugin/unraid/Tools/
rm -rf /boot/config/plugins/varaverk/Plugin/unraid/tools

# Remove originals from old locations
rm /boot/config/plugins/varaverk/System_Essentials/unraid_api_key_renew.sh
rm /boot/config/plugins/varaverk/System_Essentials/mover_stop.sh
rm /boot/config/plugins/varaverk/System_Essentials/user_scripts_stop.sh
rm /boot/config/plugins/varaverk/System_Essentials/php_fpm_max_children.sh
rm /boot/config/plugins/varaverk/Watchdogs/System/webgui_watchdog.sh
rm /boot/config/plugins/varaverk/Tools/recreate_shares.sh
rm /boot/config/plugins/varaverk/user_script_plug-in.sh

echo "=== New Plugin/unraid/ structure ==="
find /boot/config/plugins/varaverk/Plugin/unraid -mindepth 1 -maxdepth 2 -type d | sort
echo ""
echo "=== Tools/ ==="
ls /boot/config/plugins/varaverk/Plugin/unraid/Tools/
echo ""
echo "=== System_Essentials/ ==="
ls /boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/
echo ""
echo "=== Watchdogs/ ==="
find /boot/config/plugins/varaverk/Plugin/unraid/Watchdogs -type f | sort

Create directories, move all 7 scripts, rename tools/ to Tools/


=== New Plugin/unraid/ structure ===
/boot/config/plugins/varaverk/Plugin/unraid/System_Essentials
/boot/config/plugins/varaverk/Plugin/unraid/Tools
/boot/config/plugins/varaverk/Plugin/unraid/Watchdogs
/boot/config/plugins/varaverk/Plugin/unraid/Watchdogs/System
/boot/config/plugins/varaverk/Plugin/unraid/api
/boot/config/plugins/varaverk/Plugin/unraid/css
/boot/config/plugins/varaverk/Plugin/unraid/event
/boot/config/plugins/varaverk/Plugin/unraid/event/disks_mounted
/boot/config/plugins/varaverk/Plugin/unraid/event/disks_unmounting
/boot/config/plugins/varaverk/Plugin/unraid/icons
/boot/config/plugins/varaverk/Plugin/unraid/include
/boot/config/plugins/varaverk/Plugin/unraid/js
/boot/config/plugins/varaverk/Plugin/unraid/pages

=== Tools/ ===
api_cache_writer.php
api_cache_writer.sh*
conf_populate.sh*
recreate_shares.sh*
remote_arr_cache_writer.sh*
storage_migrate.sh*

=== System_Essentials/ ===
mover_stop.sh*
php_fpm_max_children.sh*
unraid_api_key_renew.sh*
user_scripts_stop.sh*

=== Watchdogs/ ===
/boot/config/plugins/varaverk/Plugin/unraid/Watchdogs/System/webgui_watchdog.sh


Files are in place. Now fix source depths in all moved scripts:


BASE=/boot/config/plugins/varaverk/Plugin/unraid

# System_Essentials/* — was "../" (1 up from System_Essentials/) → now "../../../" (3 up from Plugin/unraid/System_Essentials/)
for f in unraid_api_key_renew.sh mover_stop.sh user_scripts_stop.sh php_fpm_max_children.sh; do
    sed -i 's|source "\$SCRIPT_DIR/../load_config.sh"|source "$SCRIPT_DIR/../../../load_config.sh"|' "$BASE/System_Essentials/$f"
done

# Tools/recreate_shares.sh — was "../" (1 up from Tools/) → now "../../../" (3 up from Plugin/unraid/Tools/)
sed -i 's|source "\$SCRIPT_DIR/../load_config.sh"|source "$SCRIPT_DIR/../../../load_config.sh"|' "$BASE/Tools/recreate_shares.sh"

# Watchdogs/System/webgui_watchdog.sh — was "../../" (2 up from Watchdogs/System/) → now "../../../../" (4 up from Plugin/unraid/Watchdogs/System/)
sed -i 's|source "\$SCRIPT_DIR/../../load_config.sh"|source "$SCRIPT_DIR/../../../../load_config.sh"|' "$BASE/Watchdogs/System/webgui_watchdog.sh"

# Verify
echo "=== Source lines after update ==="
grep "source.*load_config" \
  "$BASE/System_Essentials/unraid_api_key_renew.sh" \
  "$BASE/System_Essentials/mover_stop.sh" \
  "$BASE/System_Essentials/user_scripts_stop.sh" \
  "$BASE/System_Essentials/php_fpm_max_children.sh" \
  "$BASE/Tools/recreate_shares.sh" \
  "$BASE/Watchdogs/System/webgui_watchdog.sh"

Fix source depths in all moved bash scripts


=== Source lines after update ===
/boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/unraid_api_key_renew.sh:source "$SCRIPT_DIR/../../../load_config.sh"
/boot/config/plugins/varaverk/Plugin/unraid/Tools/recreate_shares.sh:source "$SCRIPT_DIR/../../../load_config.sh"
/boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/user_scripts_stop.sh:source "$SCRIPT_DIR/../../../load_config.sh"
/boot/config/plugins/varaverk/Plugin/unraid/Watchdogs/System/webgui_watchdog.sh:source "$SCRIPT_DIR/../../../../load_config.sh"
/boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/mover_stop.sh:source "$SCRIPT_DIR/../../../load_config.sh"
/boot/config/plugins/varaverk/Plugin/unraid/System_Essentials/php_fpm_max_children.sh:source "$SCRIPT_DIR/../../../load_config.sh"


Good. Now update master.conf — both live and template:


for conf in \
  /boot/config/plugins/varaverk/Configurations/master.conf \
  /boot/config/plugins/varaverk/Deployment/conf_templates/master.conf; do
  sed -i \
    -e 's|"System_Essentials/unraid_api_key_renew.sh"|"Plugin/unraid/System_Essentials/unraid_api_key_renew.sh"|g' \
    -e 's|"System_Essentials/php_fpm_max_children.sh"|"Plugin/unraid/System_Essentials/php_fpm_max_children.sh"|g' \
    -e 's|"System_Essentials/user_scripts_stop.sh"|"Plugin/unraid/System_Essentials/user_scripts_stop.sh"|g' \
    -e 's|"System_Essentials/mover_stop.sh"|"Plugin/unraid/System_Essentials/mover_stop.sh"|g' \
    -e 's|"Watchdogs/System/webgui_watchdog.sh"|"Plugin/unraid/Watchdogs/System/webgui_watchdog.sh"|g' \
    "$conf"
  echo "Updated: $conf"
done

# Verify in live conf
grep -n "unraid_api_key_renew\|php_fpm_max_children\|user_scripts_stop\|mover_stop\|webgui_watchdog" \
  /boot/config/plugins/varaverk/Configurations/master.conf

Update script paths in master.conf and conf template


Updated: /boot/config/plugins/varaverk/Configurations/master.conf
Updated: /boot/config/plugins/varaverk/Deployment/conf_templates/master.conf
284:        "Plugin/unraid/System_Essentials/unraid_api_key_renew.sh"   # re-register Varaverk API key — registry is ephemeral
288:        "Plugin/unraid/System_Essentials/php_fpm_max_children.sh"   # WebGUI performance tuning
300:        "Plugin/unraid/System_Essentials/user_scripts_stop.sh"        # stop background scripts before they start new ops
301:        "Fallback/fallback.sh --stop"                   # gracefully stop fallback (not caught by user_scripts_stop)
303:        "Plugin/unraid/System_Essentials/mover_stop.sh"               # stop mover after rsync (they conflict on same files)
317:        "Plugin/unraid/System_Essentials/unraid_api_key_renew.sh"   # re-register API key if registry lost it
326:        "Plugin/unraid/Watchdogs/System/webgui_watchdog.sh"    # WebGUI availability — nginx → php-fpm → emhttp
833:# Seconds to wait before mover_stop.sh sends SIGTERM to the mover process.


Now the PHP references — confform.php, scheduler.php, arrs.php, storage.php:


24	    'Watchdogs/docker_watchdog.sh'                    => ['Docker Watchdog'],
25	    'Watchdogs/resource_watchdog.sh'                  => ['Pressure Levels'],
26	    'Watchdogs/System/network_watchdog.sh'            => ['Network Watchdog'],
27	    'Watchdogs/System/webgui_watchdog.sh'             => ['WebGUI Watchdog'],
28	    // Media
29	    'Media/media_cleaner.sh'                          => ['Media Cleaner'],

    'Watchdogs/docker_watchdog.sh'                    => ['Docker Watchdog'],
    'Watchdogs/resource_watchdog.sh'                  => ['Pressure Levels'],
    'Watchdogs/System/network_watchdog.sh'            => ['Network Watchdog'],
    'Watchdogs/System/webgui_watchdog.sh'             => ['WebGUI Watchdog'],
    // Media
    'Media/media_cleaner.sh'                          => ['Media Cleaner'],


Edit

'Watchdogs/System/webgui_watchdog.sh'             => ['WebGUI Watchdog'],

'Plugin/unraid/Watchdogs/System/webgui_watchdog.sh' => ['WebGUI Watchdog'],


The file /boot/config/plugins/varaverk/Plugin/unraid/include/confform.php has been updated successfully. (file state is current in your context — no need to Read it back)

'Watchdogs/docker_watchdog.sh'                    => ['Docker Watchdog'],

'Watchdogs/resource_watchdog.sh'                  => ['Pressure Levels'],

'Watchdogs/System/network_watchdog.sh'            => ['Network Watchdog'],

-    'Watchdogs/System/webgui_watchdog.sh'             => ['WebGUI Watchdog'],

+    'Plugin/unraid/Watchdogs/System/webgui_watchdog.sh' => ['WebGUI Watchdog'],

'Media/media_cleaner.sh'                          => ['Media Cleaner'],

'Media/media_shares_permissions.sh'               => ['Media Permissions'],


148
149	// Parse user_script_plug-in.sh into an array of script blocks.
150	// Each block: title, schedule, desc (array of lines), scripts (array of {rel, cron})
151	function vv_parse_user_script_template(): array {
152	    $file   = SCRIPTS_DIR . '/user_script_plug-in.sh';
153	    if (!file_exists($file)) return [];
154	    $lines  = file($file, FILE_IGNORE_NEW_LINES);
155	    $prefix = rtrim(SCRIPTS_DIR, '/') . '/';

// Parse user_script_plug-in.sh into an array of script blocks.
// Each block: title, schedule, desc (array of lines), scripts (array of {rel, cron})
function vv_parse_user_script_template(): array {
    $file   = SCRIPTS_DIR . '/user_script_plug-in.sh';
    if (!file_exists($file)) return [];
    $lines  = file($file, FILE_IGNORE_NEW_LINES);
    $prefix = rtrim(SCRIPTS_DIR, '/') . '/';


$file   = SCRIPTS_DIR . '/user_script_plug-in.sh';

$file   = SCRIPTS_DIR . '/Plugin/unraid/user_script_plug-in.sh';


The file /boot/config/plugins/varaverk/Plugin/unraid/include/scheduler.php has been updated successfully. (file state is current in your context — no need to Read it back)

// Parse user_script_plug-in.sh into an array of script blocks.

// Each block: title, schedule, desc (array of lines), scripts (array of {rel, cron})

function vv_parse_user_script_template(): array {

-    $file   = SCRIPTS_DIR . '/user_script_plug-in.sh';

+    $file   = SCRIPTS_DIR . '/Plugin/unraid/user_script_plug-in.sh';

$lines  = file($file, FILE_IGNORE_NEW_LINES);

$prefix = rtrim(SCRIPTS_DIR, '/') . '/';


Now update the `tools/` → `Tools/` references in scheduler.php's `vv_tools_scripts()`:


    // Platform adapter tools (Plugin/<platform>/tools/)
    foreach (glob(SCRIPTS_DIR . '/Plugin/*/tools') ?: [] as $toolsDir) {
        $platform = basename(dirname($toolsDir));
        $collect($toolsDir, "Plugin/$platform/tools/");
    }

    // Platform adapter tools (Plugin/<platform>/Tools/)
    foreach (glob(SCRIPTS_DIR . '/Plugin/*/Tools') ?: [] as $toolsDir) {
        $platform = basename(dirname($toolsDir));
        $collect($toolsDir, "Plugin/$platform/Tools/");
    }


    // Platform adapter tools (Plugin/<platform>/tools/)
    foreach (glob(SCRIPTS_DIR . '/Plugin/*/tools') ?: [] as $toolsDir) {
        $platform = basename(dirname($toolsDir));
        $collect($toolsDir, "Plugin/$platform/tools/");
    }

    // Platform adapter tools (Plugin/<platform>/Tools/)
    foreach (glob(SCRIPTS_DIR . '/Plugin/*/Tools') ?: [] as $toolsDir) {
        $platform = basename(dirname($toolsDir));
        $collect($toolsDir, "Plugin/$platform/Tools/");
    }

$collect(SCRIPTS_DIR . '/Tools', 'Tools/');

-    // Platform adapter tools (Plugin/<platform>/tools/)

-    foreach (glob(SCRIPTS_DIR . '/Plugin/*/tools') ?: [] as $toolsDir) {

+    // Platform adapter tools (Plugin/<platform>/Tools/)

+    foreach (glob(SCRIPTS_DIR . '/Plugin/*/Tools') ?: [] as $toolsDir) {

-        $collect($toolsDir, "Plugin/$platform/tools/");

+        $collect($toolsDir, "Plugin/$platform/Tools/");

usort($scripts, fn($a, $b) => strcmp($a['label'], $b['label']));


Now arrs.php and storage.php:


$script = dirname(__DIR__) . '/tools/remote_arr_cache_writer.sh';

$script = dirname(__DIR__) . '/Tools/remote_arr_cache_writer.sh';


The file /boot/config/plugins/varaverk/Plugin/unraid/api/arrs.php has been updated successfully. (file state is current in your context — no need to Read it back)

<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

// ── Manual remote refresh — runs remote_arr_cache_writer for one host ─────────
$_action = trim($_GET['action'] ?? '');
if ($_action === 'refresh_remote') {
    $host = strtolower(trim($_GET['host'] ?? ''));
    if (!preg_match('/^host\d+$/', $host)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid host']); exit;
    }
    $script = dirname(__DIR__) . '/tools/remote_arr_cache_writer.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'remote_arr_cache_writer.sh not found']); exit;
    }
    set_time_limit(30);
    $out = []; $exit = 0;
    exec('bash ' . escapeshellarg($script) . ' --host=' . escapeshellarg(strtoupper($host)) . ' 2>&1', $out, $exit);

    $cacheFile = VV_CACHE_DIR . '/arrs_remote_' . $host . '.json';
    $node = null;
    if (file_exists($cacheFile)) {
        $node = json_decode(file_get_contents($cacheFile), true) ?: null;
        if ($node) {
            $node['cached']    = true;
            $node['cache_age'] = time() - (int)filemtime($cacheFile);
        }
    }
    // Bust the main arrs cache so next poll gets fresh data
    @unlink(VV_CACHE_DIR . '/arrs.json');
    echo json_encode(['ok' => $exit === 0, 'node' => $node, 'output' => implode("\n", $out)]);
    exit;
}
unset($_action);

// ── Normal data load ──────────────────────────────────────────────────────────
$_vv_cached = vv_cache_read('arrs', 300);
if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; }
unset($_vv_cached);

require_once dirname(__DIR__) . '/include/arrs.php';
echo json_encode(vv_arrs_all());

-    $script = dirname(__DIR__) . '/tools/remote_arr_cache_writer.sh';

+    $script = dirname(__DIR__) . '/Tools/remote_arr_cache_writer.sh';


$script = dirname(__DIR__) . '/tools/storage_migrate.sh';

$script = dirname(__DIR__) . '/Tools/storage_migrate.sh';


The file /boot/config/plugins/varaverk/Plugin/unraid/api/storage.php has been updated successfully. (file state is current in your context — no need to Read it back)

<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

$action = $_GET['action'] ?? $_POST['action'] ?? '';

// ── Boot device detection ─────────────────────────────────────────────────────
function vv_storage_detect_transport(): string {
    $part = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
    if (!$part) return 'unknown';
    $disk = trim(shell_exec("lsblk -no pkname " . escapeshellarg($part) . " 2>/dev/null") ?: '');
    if (!$disk) return 'unknown';
    return strtolower(trim(shell_exec("lsblk -dno TRAN /dev/" . escapeshellarg($disk) . " 2>/dev/null") ?: 'unknown'));
}

// ── Current mode status ───────────────────────────────────────────────────────
if ($action === 'status') {
    $transport   = vv_storage_detect_transport();
    $detected    = ($transport === 'usb') ? 'flash' : 'internal';
    $currentDir  = SCRIPTS_DIR;
    $internalDir = '/boot/config/plugins/varaverk';
    $flashDir    = '/mnt/user/appdata/Varaverk';
    $currentMode = ($currentDir === $internalDir) ? 'internal'
                 : ($currentDir === $flashDir     ? 'flash' : 'custom');

    $myHost  = vv_detect_host();
    $vars    = vv_conf_vars();
    $confKey = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
    $confVal = $vars[$confKey] ?? null;

    // Boot device name for display
    $bootPart = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
    $bootDisk = $bootPart ? trim(shell_exec("lsblk -no pkname " . escapeshellarg($bootPart) . " 2>/dev/null") ?: '') : '';

    echo json_encode([
        'ok'           => true,
        'current_mode' => $currentMode,
        'current_dir'  => $currentDir,
        'internal_dir' => $internalDir,
        'flash_dir'    => $flashDir,
        'transport'    => $transport,
        'detected'     => $detected,
        'boot_disk'    => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
        'conf_key'     => $confKey,
        'conf_val'     => $confVal,
        'array_started'=> is_dir('/mnt/user') && count(scandir('/mnt/user')) > 2,
    ]);
    exit;
}

// ── Run migration ─────────────────────────────────────────────────────────────
if ($action === 'migrate' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $to = trim($_POST['to'] ?? '');
    if (!in_array($to, ['internal', 'flash'], true)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid target: must be internal or flash']);
        exit;
    }

    $script = dirname(__DIR__) . '/tools/storage_migrate.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh not found']);
        exit;
    }

    set_time_limit(300);
    $output = [];
    $exit   = 0;
    exec('bash ' . escapeshellarg($script) . ' --to=' . escapeshellarg($to) . ' 2>&1', $output, $exit);

    echo json_encode([
        'ok'     => $exit === 0,
        'exit'   => $exit,
        'output' => implode("\n", $output),
    ]);
    exit;
}

// ── Auto-detect and write to conf ─────────────────────────────────────────────
if ($action === 'detect' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $transport = vv_storage_detect_transport();
    $detected  = ($transport === 'usb') ? 'false' : 'true';
    $myHost    = vv_detect_host();
    $confKey   = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
    $confFile  = $myHost . '.conf';

    $results = vv_conf_write_changes([[
        'file'  => $confFile,
        'key'   => $confKey,
        'value' => $detected,
        'type'  => 'scalar',
    ]]);

    $ok = !in_array(false, $results, true);
    echo json_encode(['ok' => $ok, 'detected' => $detected, 'transport' => $transport]);
    exit;
}

// ── Unraid API key status ─────────────────────────────────────────────────────
// Keys live in host*.conf (private, not master.conf):
//   host1.conf: HOST1_UNRAID_API_KEY (own) + HOST2_UNRAID_API_KEY (HOST1's access to HOST2)
//   host2.conf: HOST2_UNRAID_API_KEY (own) + HOST1_UNRAID_API_KEY (HOST2's access to HOST1)
if ($action === 'api_status') {
    require_once dirname(__DIR__) . '/include/unraid_api.php';
    $localStatus = vv_api_get_status();
    $vars        = vv_conf_vars();
    $myHost      = vv_detect_host();
    $myId        = strtoupper($myHost);

    $hosts = [];
    foreach ($vars as $k => $v) {
        if (!preg_match('/^HOST(\d+)$/', $k, $m) || !$v) continue;
        $id      = 'HOST' . $m[1];
        $keyVar  = $id . '_UNRAID_API_KEY';
        $key     = $vars[$keyVar] ?? '';
        $isLocal = ($id === $myId);
        // For local: key is Varaverk_HOST1 registered on own machine
        // For remote: key is Varaverk_HOST1 registered on HOST2's machine (stored in host1.conf)
        $hosts[] = [
            'host_id'     => $id,
            'hostname'    => $v,
            'is_local'    => $isLocal,
            'key_var'     => $keyVar,
            'key_name'    => 'Varaverk_' . ($isLocal ? $myId : $myId), // Varaverk_HOST1 on that registry
            'key_present' => !empty($key),
            'key_preview' => $key ? substr($key, 0, 8) . '...' . substr($key, -4) : null,
            'api_ok'      => $isLocal
                             ? (!$localStatus['key_missing'] && $localStatus['available'])
                             : !empty($key),
        ];
    }
    usort($hosts, fn($a, $b) => strcmp($a['host_id'], $b['host_id']));

    echo json_encode([
        'ok'        => true,
        'my_id'     => $myId,
        'hosts'     => $hosts,
        'fallbacks' => $localStatus['fallbacks'],
    ]);
    exit;
}

// ── Setup/renew API keys (local + all partners via SSH) ───────────────────────
if ($action === 'setup_apikeys' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $script = SCRIPTS_DIR . '/System_Essentials/unraid_api_key_renew.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'unraid_api_key_renew.sh not found']); exit;
    }
    $allHosts = ($_POST['all_hosts'] ?? '0') === '1';
    $flags    = $allHosts ? ' --all-hosts' : '';
    set_time_limit(60);
    $output = []; $exit = 0;
    exec('bash ' . escapeshellarg($script) . $flags . ' 2>&1', $output, $exit);
    echo json_encode(['ok' => $exit === 0, 'exit' => $exit, 'output' => implode("\n", $output)]);
    exit;
}

echo json_encode(['ok' => false, 'error' => 'Unknown action']);

-    $script = dirname(__DIR__) . '/tools/storage_migrate.sh';

+    $script = dirname(__DIR__) . '/Tools/storage_migrate.sh';


$script = SCRIPTS_DIR . '/System_Essentials/unraid_api_key_renew.sh';

$script = SCRIPTS_DIR . '/Plugin/unraid/System_Essentials/unraid_api_key_renew.sh';


<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';

$action = $_GET['action'] ?? $_POST['action'] ?? '';

// ── Boot device detection ─────────────────────────────────────────────────────
function vv_storage_detect_transport(): string {
    $part = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
    if (!$part) return 'unknown';
    $disk = trim(shell_exec("lsblk -no pkname " . escapeshellarg($part) . " 2>/dev/null") ?: '');
    if (!$disk) return 'unknown';
    return strtolower(trim(shell_exec("lsblk -dno TRAN /dev/" . escapeshellarg($disk) . " 2>/dev/null") ?: 'unknown'));
}

// ── Current mode status ───────────────────────────────────────────────────────
if ($action === 'status') {
    $transport   = vv_storage_detect_transport();
    $detected    = ($transport === 'usb') ? 'flash' : 'internal';
    $currentDir  = SCRIPTS_DIR;
    $internalDir = '/boot/config/plugins/varaverk';
    $flashDir    = '/mnt/user/appdata/Varaverk';
    $currentMode = ($currentDir === $internalDir) ? 'internal'
                 : ($currentDir === $flashDir     ? 'flash' : 'custom');

    $myHost  = vv_detect_host();
    $vars    = vv_conf_vars();
    $confKey = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
    $confVal = $vars[$confKey] ?? null;

    // Boot device name for display
    $bootPart = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: '');
    $bootDisk = $bootPart ? trim(shell_exec("lsblk -no pkname " . escapeshellarg($bootPart) . " 2>/dev/null") ?: '') : '';

    echo json_encode([
        'ok'           => true,
        'current_mode' => $currentMode,
        'current_dir'  => $currentDir,
        'internal_dir' => $internalDir,
        'flash_dir'    => $flashDir,
        'transport'    => $transport,
        'detected'     => $detected,
        'boot_disk'    => $bootDisk ? '/dev/' . $bootDisk : 'unknown',
        'conf_key'     => $confKey,
        'conf_val'     => $confVal,
        'array_started'=> is_dir('/mnt/user') && count(scandir('/mnt/user')) > 2,
    ]);
    exit;
}

// ── Run migration ─────────────────────────────────────────────────────────────
if ($action === 'migrate' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $to = trim($_POST['to'] ?? '');
    if (!in_array($to, ['internal', 'flash'], true)) {
        echo json_encode(['ok' => false, 'error' => 'Invalid target: must be internal or flash']);
        exit;
    }

    $script = dirname(__DIR__) . '/Tools/storage_migrate.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh not found']);
        exit;
    }

    set_time_limit(300);
    $output = [];
    $exit   = 0;
    exec('bash ' . escapeshellarg($script) . ' --to=' . escapeshellarg($to) . ' 2>&1', $output, $exit);

    echo json_encode([
        'ok'     => $exit === 0,
        'exit'   => $exit,
        'output' => implode("\n", $output),
    ]);
    exit;
}

// ── Auto-detect and write to conf ─────────────────────────────────────────────
if ($action === 'detect' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $transport = vv_storage_detect_transport();
    $detected  = ($transport === 'usb') ? 'false' : 'true';
    $myHost    = vv_detect_host();
    $confKey   = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL';
    $confFile  = $myHost . '.conf';

    $results = vv_conf_write_changes([[
        'file'  => $confFile,
        'key'   => $confKey,
        'value' => $detected,
        'type'  => 'scalar',
    ]]);

    $ok = !in_array(false, $results, true);
    echo json_encode(['ok' => $ok, 'detected' => $detected, 'transport' => $transport]);
    exit;
}

// ── Unraid API key status ─────────────────────────────────────────────────────
// Keys live in host*.conf (private, not master.conf):
//   host1.conf: HOST1_UNRAID_API_KEY (own) + HOST2_UNRAID_API_KEY (HOST1's access to HOST2)
//   host2.conf: HOST2_UNRAID_API_KEY (own) + HOST1_UNRAID_API_KEY (HOST2's access to HOST1)
if ($action === 'api_status') {
    require_once dirname(__DIR__) . '/include/unraid_api.php';
    $localStatus = vv_api_get_status();
    $vars        = vv_conf_vars();
    $myHost      = vv_detect_host();
    $myId        = strtoupper($myHost);

    $hosts = [];
    foreach ($vars as $k => $v) {
        if (!preg_match('/^HOST(\d+)$/', $k, $m) || !$v) continue;
        $id      = 'HOST' . $m[1];
        $keyVar  = $id . '_UNRAID_API_KEY';
        $key     = $vars[$keyVar] ?? '';
        $isLocal = ($id === $myId);
        // For local: key is Varaverk_HOST1 registered on own machine
        // For remote: key is Varaverk_HOST1 registered on HOST2's machine (stored in host1.conf)
        $hosts[] = [
            'host_id'     => $id,
            'hostname'    => $v,
            'is_local'    => $isLocal,
            'key_var'     => $keyVar,
            'key_name'    => 'Varaverk_' . ($isLocal ? $myId : $myId), // Varaverk_HOST1 on that registry
            'key_present' => !empty($key),
            'key_preview' => $key ? substr($key, 0, 8) . '...' . substr($key, -4) : null,
            'api_ok'      => $isLocal
                             ? (!$localStatus['key_missing'] && $localStatus['available'])
                             : !empty($key),
        ];
    }
    usort($hosts, fn($a, $b) => strcmp($a['host_id'], $b['host_id']));

    echo json_encode([
        'ok'        => true,
        'my_id'     => $myId,
        'hosts'     => $hosts,
        'fallbacks' => $localStatus['fallbacks'],
    ]);
    exit;
}

// ── Setup/renew API keys (local + all partners via SSH) ───────────────────────
if ($action === 'setup_apikeys' && $_SERVER['REQUEST_METHOD'] === 'POST') {
    $script = SCRIPTS_DIR . '/System_Essentials/unraid_api_key_renew.sh';
    if (!file_exists($script)) {
        echo json_encode(['ok' => false, 'error' => 'unraid_api_key_renew.sh not found']); exit;
    }
    $allHosts = ($_POST['all_hosts'] ?? '0') === '1';
    $flags    = $allHosts ? ' --all-hosts' : '';
    set_time_limit(60);
    $output = []; $exit = 0;
    exec('bash ' . escapeshellarg($script) . $flags . ' 2>&1', $output, $exit);
    echo json_encode(['ok' => $exit === 0, 'exit' => $exit, 'output' => implode("\n", $output)]);
    exit;
}

echo json_encode(['ok' => false, 'error' => 'Unknown action']);

-    $script = SCRIPTS_DIR . '/System_Essentials/unraid_api_key_renew.sh';

+    $script = SCRIPTS_DIR . '/Plugin/unraid/System_Essentials/unraid_api_key_renew.sh';


Now update schedule.json keys:


72	    "Plugin\/unraid\/tools\/api_cache_writer.sh": {
73	        "id": "Plugin\/unraid\/tools\/api_cache_writer.sh",
74	        "enabled": true,
75	        "cron": "* * * * *",
76	        "log_enabled": false,
77	        "updated": "2026-06-05T00:00:00+00:00"
78	    },
79	    "Plugin\/unraid\/tools\/remote_arr_cache_writer.sh": {
80	        "id": "Plugin\/unraid\/tools\/remote_arr_cache_writer.sh",
81	        "enabled": true,
82	        "cron": "0 *\/2 * * *",
83	        "log_enabled": false,
84	        "updated": "2026-06-05T00:00:00+00:00"
85	    }

    "Plugin\/unraid\/tools\/api_cache_writer.sh": {
        "id": "Plugin\/unraid\/tools\/api_cache_writer.sh",
        "enabled": true,
        "cron": "* * * * *",
        "log_enabled": false,
        "updated": "2026-06-05T00:00:00+00:00"
    },
    "Plugin\/unraid\/tools\/remote_arr_cache_writer.sh": {
        "id": "Plugin\/unraid\/tools\/remote_arr_cache_writer.sh",
        "enabled": true,
        "cron": "0 *\/2 * * *",
        "log_enabled": false,
        "updated": "2026-06-05T00:00:00+00:00"
    }


The file /boot/config/plugins/varaverk/schedule.json has been updated. All occurrences were successfully replaced. (file state is current in your context — no need to Read it back)

-    "Plugin\/unraid\/tools\/api_cache_writer.sh": {

-        "id": "Plugin\/unraid\/tools\/api_cache_writer.sh",

+    "Plugin\/unraid\/Tools\/api_cache_writer.sh": {

+        "id": "Plugin\/unraid\/Tools\/api_cache_writer.sh",

-    "Plugin\/unraid\/tools\/remote_arr_cache_writer.sh": {

-        "id": "Plugin\/unraid\/tools\/remote_arr_cache_writer.sh",

+    "Plugin\/unraid\/Tools\/remote_arr_cache_writer.sh": {

+        "id": "Plugin\/unraid\/Tools\/remote_arr_cache_writer.sh",


Now rebuild cron and verify the full Tools list picks up correctly:


Bash

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/scheduler.php';

// Verify tools list
\$tools = vv_tools_scripts();
echo '=== Tools card (' . count(\$tools) . ') ===\n';
foreach (\$tools as \$t) echo '  ' . \$t['id'] . '\n';

// Rebuild cron
\$ok = vv_cron_rebuild(vv_schedule_load());
echo '\nCron rebuild: ' . (\$ok ? 'OK' : 'FAIL') . '\n';
" 2>&1

Verify tools list and rebuild cron


=== Tools card (16) ===\n  Tools/bulk_permissions_repair.sh\n  Tools/claude_startup.sh\n  Plugin/unraid/Tools/conf_populate.sh\n  Tools/container_data_export.sh\n  Tools/docker_prune_images.sh\n  Tools/emby_database_repair.sh\n  Tools/emby_to_lidarr_sync.sh\n  Tools/emby_to_radarr_sync.sh\n  Tools/emby_to_sonarr_sync.sh\n  Tools/fallback_state_reset.sh\n  Tools/ramdisk_stop.sh\n  Plugin/unraid/Tools/recreate_shares.sh\n  Tools/smart_long_test.sh\n  Plugin/unraid/Tools/storage_migrate.sh\n  Tools/watchdog_skip_list_manager.sh\n  Tools/zfs_pool_scrub.sh\n\nCron rebuild: OK\n


16 tools, cron rebuilt. `recreate_shares` now appears in the Tools card alongside the other adapter tools. Let me do a final sweep to confirm no stale references remain:


grep -rn "unraid/tools/" /boot/config/plugins/varaverk \
  --include="*.sh" --include="*.php" --include="*.json" --include="*.conf" \
  | grep -v "Binary\|#.*tools/"
echo "---"
# Confirm old paths are gone
for old in \
  "System_Essentials/unraid_api_key_renew.sh" \
  "System_Essentials/mover_stop.sh" \
  "System_Essentials/user_scripts_stop.sh" \
  "System_Essentials/php_fpm_max_children.sh" \
  "Watchdogs/System/webgui_watchdog.sh" \
  "Tools/recreate_shares.sh"; do
  if [ -f "/boot/config/plugins/varaverk/$old" ]; then
    echo "STILL EXISTS: $old"
  fi
done
echo "Old file check done"

Verify no stale tools/ references and old files are gone


---
Old file check done


Clean. Everything's in order.

**What moved:**

```
Plugin/unraid/
├── System_Essentials/
│   ├── mover_stop.sh
│   ├── php_fpm_max_children.sh
│   ├── unraid_api_key_renew.sh
│   └── user_scripts_stop.sh
├── Tools/               ← renamed from tools/, + recreate_shares added
│   ├── api_cache_writer.php
│   ├── api_cache_writer.sh
│   ├── conf_populate.sh
│   ├── recreate_shares.sh   ← moved from Tools/
│   ├── remote_arr_cache_writer.sh
│   └── storage_migrate.sh
├── Watchdogs/
│   └── System/
│       └── webgui_watchdog.sh
└── user_script_plug-in.sh
```

Originals deleted. `master.conf` (live + template), all PHP references, `schedule.json`, and cron all updated and verified.


we are going to have to update all cards in scheduler to merge /Varaverk/plugin/****/ folders of the same name like we did with tools


Let me read the full scheduler page and all its data-gathering functions to map every card:


grep -n "function vv_\|vv_tools\|vv_custom\|vv_orch\|vv_monitor\|vv_watchdog\|vv_script\|vv_rsync\|vv_parse" \
  /boot/config/plugins/varaverk/Plugin/unraid/include/scheduler.php | head -60

List all scheduler helper functions


11:function vv_pretty_label(string $slug): string {
15:function vv_schedule_load(): array {
21:function vv_schedule_save(array $schedule): bool {
27:function vv_schedule_update(string $id, bool $enabled, string $cron, bool $log_enabled = false): bool {
40:function vv_schedule_update_batch(array $entries): bool {
57:function vv_job_flags(string $id): string {
62:function vv_job_log_path(string $id): string {
66:function vv_job_stat_path(string $id): string {
70:function vv_cron_rebuild(array $schedule): bool {
88:            foreach (vv_parse_conf_array_full($confRaw, $var) as $item) $childToOrch[$item['path']] = $orchId;
134:function vv_script_suggested_cron(string $path): array {
151:function vv_parse_user_script_template(): array {
208:function vv_script_description(string $path): string {
233:function vv_tools_scripts(): array {
249:                'desc'        => vv_script_description($path),
270:function vv_custom_scripts(): array {
280:            'desc'        => vv_script_description($path),
290:function vv_rsync_standalone(string $flagName): array {
300:function vv_orch_conf_arrays(string $orchPath): array {
308:function vv_script_library(): array {
336:function vv_folders_load(): array {
346:function vv_job_tree(): array {
363:        $suggested = $isEvent ? ['cron' => '', 'label' => ''] : vv_script_suggested_cron($path);
367:            'desc'            => vv_script_description($path),
374:            'children'        => vv_script_children($path, $schedule),
375:            'conf_arrays'     => vv_orch_conf_arrays($path),
397:function vv_parse_conf_array(string $conf, string $varName): array {
411:// Like vv_parse_conf_array but includes commented entries.
413:function vv_parse_conf_array_full(string $conf, string $varName): array {
431:function vv_conf_script_map(): array {
455:function vv_conf_flag_value(string $name): bool {
464:function vv_conf_flag_set(string $name, bool $value): bool {
479:function vv_conf_toggle_script(string $rel, bool $enable): bool {
512:function vv_script_children(string $orchPath, array $schedule): array {
530:        $suggested   = vv_script_suggested_cron("$scriptsDir/$rel");
534:            'desc'            => vv_script_description("$scriptsDir/$rel"),
559:            foreach (vv_parse_conf_array_full($confRaw, $varName) as $item) $addChild($item['path']);
581:function vv_script_header(string $path): string {
602:function vv_script_header_clean(string $path): string {
603:    $raw   = vv_script_header($path);
618:function vv_readme_section(string $readmePath, callable $matcher, int $maxChars = 3000): string {


grep -n "^\$\|= vv_\|vv_tools\|vv_custom\|vv_orch\|vv_monitor\|vv_watchdog\|vv_parse\|vv_script_lib\|vv_rsync\|vv_cron" \
  /boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php | head -40

List all data vars populated at top of scheduler page


3:
7:
11:$_vv_my_host       = vv_detect_host();
13:
14:$tree    = vv_job_tree();
15:$customs = vv_custom_scripts();
16:$tools   = vv_tools_scripts();
17:$_library = vv_script_library();
18:$_folders = vv_folders_load();
19:
25:
44:
56:
76:
79:
83:
86:
109:            <?php $orchLog = vv_job_log_path($orch['id']); ?>
122:
127:                  $_rsync    = vv_rsync_standalone($child['flag_name']);
155:                    <?php $childLog = vv_job_log_path($child['id']); ?>
191:                    <?php $childLog = vv_job_log_path($child['id']); ?>
208:
211:
244:                  <?php $tsLog = vv_job_log_path($ts['id']); ?>
260:
277:
307:                      <?php $csLog = vv_job_log_path($cs['id']); ?>
318:
345:                  <?php $csLog = vv_job_log_path($cs['id']); ?>
361:
363:
372:
374:
406:
409:
418:            <?php $confFiles = vv_get_conf_files(); ?>
431:
472:
483:


1	<?php
2	require_once dirname(__DIR__) . '/include/scheduler.php';
3
4	// Setup mode — auto-open a conf file and force the editing sequence
5	$vv_setup_conf = preg_match('/^[\w.]+\.conf$/', $_GET['vv_setup'] ?? '')
6	    ? $_GET['vv_setup'] : '';
7
8	// Deep-link from Monitor → open a specific script on load (e.g. ?tab=scheduler&vv_script=Media/x.sh)
9	$vv_open_script = preg_match('#^[\w./-]+\.sh$#', $_GET['vv_script'] ?? '')
10	    ? $_GET['vv_script'] : '';
11	$_vv_my_host       = vv_detect_host();
12	$_vv_local_host_conf = ($_vv_my_host !== 'unknown') ? $_vv_my_host . '.conf' : 'host1.conf';
13
14	$tree    = vv_job_tree();
15	$customs = vv_custom_scripts();
16	$tools   = vv_tools_scripts();
17	$_library = vv_script_library();
18	$_folders = vv_folders_load();
19
20	// Full README and Manual for collapsed accordion panels
21	$readmePath = SCRIPTS_DIR . '/README.md';
22	$manualPath = SCRIPTS_DIR . '/Manual.md';
23	$readmeText = file_exists($readmePath) ? file_get_contents($readmePath) : '';
24	$manualText = file_exists($manualPath) ? file_get_contents($manualPath) : '';
25
26	// Repository tree: all .sh + .md files in git folder order, minus Plugin/
27	$_repoFiles  = [];
28	$_repoBase   = rtrim(SCRIPTS_DIR, '/') . '/';
29	$_repoSkip   = ['Plugin', '.git'];
30	try {
31	    $_ri = new RecursiveIteratorIterator(
32	        new RecursiveDirectoryIterator(SCRIPTS_DIR, RecursiveDirectoryIterator::SKIP_DOTS)
33	    );
34	    foreach ($_ri as $_rf) {
35	        if (!$_rf->isFile()) continue;
36	        if (!preg_match('/\.(sh|md)$/i', $_rf->getFilename())) continue;
37	        $_rel   = ltrim(str_replace($_repoBase, '', $_rf->getPathname()), '/');
38	        $_parts = explode('/', $_rel);
39	        if (in_array($_parts[0], $_repoSkip)) continue;
40	        $_repoFiles[] = $_rel;
41	    }
42	} catch (Exception $_re) {}
43	sort($_repoFiles);
44
45	// Docs tree: README and Manual with their per-module children
46	$_mainReadme   = in_array('README.md', $_repoFiles) ? 'README.md' : null;
47	$_childReadmes = array_values(array_filter($_repoFiles, function($f) {
48	    return preg_match('/^README/i', basename($f)) && $f !== 'README.md';
49	}));
50	sort($_childReadmes);

<?php
require_once dirname(__DIR__) . '/include/scheduler.php';

// Setup mode — auto-open a conf file and force the editing sequence
$vv_setup_conf = preg_match('/^[\w.]+\.conf$/', $_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_local_host_conf = ($_vv_my_host !== 'unknown') ? $_vv_my_host . '.conf' : 'host1.conf';

$tree    = vv_job_tree();
$customs = vv_custom_scripts();
$tools   = vv_tools_scripts();
$_library = vv_script_library();
$_folders = vv_folders_load();

// Full README and Manual for collapsed accordion panels
$readmePath = SCRIPTS_DIR . '/README.md';
$manualPath = SCRIPTS_DIR . '/Manual.md';
$readmeText = file_exists($readmePath) ? file_get_contents($readmePath) : '';
$manualText = file_exists($manualPath) ? file_get_contents($manualPath) : '';

// Repository tree: all .sh + .md files in git folder order, minus Plugin/
$_repoFiles  = [];
$_repoBase   = rtrim(SCRIPTS_DIR, '/') . '/';
$_repoSkip   = ['Plugin', '.git'];
try {
    $_ri = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator(SCRIPTS_DIR, RecursiveDirectoryIterator::SKIP_DOTS)
    );
    foreach ($_ri as $_rf) {
        if (!$_rf->isFile()) continue;
        if (!preg_match('/\.(sh|md)$/i', $_rf->getFilename())) continue;
        $_rel   = ltrim(str_replace($_repoBase, '', $_rf->getPathname()), '/');
        $_parts = explode('/', $_rel);
        if (in_array($_parts[0], $_repoSkip)) continue;
        $_repoFiles[] = $_rel;
    }
} catch (Exception $_re) {}
sort($_repoFiles);

// Docs tree: README and Manual with their per-module children
$_mainReadme   = in_array('README.md', $_repoFiles) ? 'README.md' : null;
$_childReadmes = array_values(array_filter($_repoFiles, function($f) {
    return preg_match('/^README/i', basename($f)) && $f !== 'README.md';
}));
sort($_childReadmes);


308	function vv_script_library(): array {
309	    $scriptsDir = SCRIPTS_DIR;
310	    $confMap    = vv_conf_script_map();
311	    $orchIds    = [];
312	    foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $p) {
313	        $orchIds[] = 'Orchestrators/' . basename($p);
314	    }
315	    $exclude = ['Plugin', '.git', 'Orchestrators', 'Custom', 'Configurations'];
316	    $library = [];
317	    try {
318	        $ri   = new RecursiveIteratorIterator(
319	            new RecursiveDirectoryIterator($scriptsDir, RecursiveDirectoryIterator::SKIP_DOTS)
320	        );
321	        $base = rtrim($scriptsDir, '/') . '/';
322	        foreach ($ri as $rf) {
323	            if (!$rf->isFile() || strtolower($rf->getExtension()) !== 'sh') continue;
324	            $rel   = ltrim(str_replace($base, '', $rf->getPathname()), '/');
325	            $parts = explode('/', $rel);
326	            if (count($parts) < 2 || in_array($parts[0], $exclude)) continue;
327	            if (in_array($rel, $orchIds) || isset($confMap[$rel])) continue;
328	            $library[] = ['id' => $rel, 'label' => vv_pretty_label(basename($rel, '.sh'))];
329	        }
330	    } catch (Exception $e) {}
331	    usort($library, fn($a, $b) => strcmp($a['id'], $b['id']));
332	    return $library;
333	}
334
335	// Load custom-script folder assignments from schedule.json (__folders key).
336	function vv_folders_load(): array {
337	    $s = vv_schedule_load();
338	    $f = $s['__folders'] ?? [];
339	    return is_array($f) ? $f : [];
340	}
341
342	// Walk the scripts repo and return the job tree.
343	// Type is derived from the saved cron value: array_start / array_stop → 'event', else 'orchestrator'.
344	// Well-known event orchs get their cron seeded from $eventDefaults when not yet in schedule.json.
345	// Any orch or custom script can carry array_start / array_stop as its cron — the event scripts fire all of them.
346	function vv_job_tree(): array {
347	    $scriptsDir = SCRIPTS_DIR;
348	    $schedule   = vv_schedule_load();
349
350	    // First-run seeds only — applied when schedule.json has no entry for these IDs yet.
351	    $eventDefaults = [
352	        'Orchestrators/array_started.sh'  => 'array_start',
353	        'Orchestrators/array_stopping.sh' => 'array_stop',
354	    ];
355
356	    $orchs = [];
357	    foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $path) {
358	        $id      = 'Orchestrators/' . basename($path);
359	        $default = $eventDefaults[$id] ?? '';
360	        $entry   = $schedule[$id] ?? ['enabled' => false, 'cron' => $default];
361	        $cron    = $entry['cron'] ?? $default;
362	        $isEvent = str_starts_with($cron, 'array_');
363	        $suggested = $isEvent ? ['cron' => '', 'label' => ''] : vv_script_suggested_cron($path);
364	        $orchs[] = [
365	            'id'              => $id,
366	            'label'           => vv_pretty_label(basename($path, '.sh')),
367	            'desc'            => vv_script_description($path),
368	            'type'            => $isEvent ? 'event' : 'orchestrator',
369	            'enabled'         => (bool)($entry['enabled'] ?? false),
370	            'cron'            => $cron,
371	            'log_enabled'     => (bool)($entry['log_enabled'] ?? false),
372	            'suggested_cron'  => $suggested['cron'],
373	            'suggested_label' => $suggested['label'],
374	            'children'        => vv_script_children($path, $schedule),
375	            'conf_arrays'     => vv_orch_conf_arrays($path),
376	        ];
377	    }
378
379	    // Events first (array_start before array_stop), then alphabetical by label.
380	    usort($orchs, function($a, $b) {
381	        $ae = $a['type'] === 'event' ? 0 : 1;
382	        $be = $b['type'] === 'event' ? 0 : 1;
383	        if ($ae !== $be) return $ae - $be;
384	        if ($ae === 0) {
385	            $as = str_contains($a['cron'], 'start') ? 0 : 1;
386	            $bs = str_contains($b['cron'], 'start') ? 0 : 1;
387	            if ($as !== $bs) return $as - $bs;

function vv_script_library(): array {
    $scriptsDir = SCRIPTS_DIR;
    $confMap    = vv_conf_script_map();
    $orchIds    = [];
    foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $p) {
        $orchIds[] = 'Orchestrators/' . basename($p);
    }
    $exclude = ['Plugin', '.git', 'Orchestrators', 'Custom', 'Configurations'];
    $library = [];
    try {
        $ri   = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($scriptsDir, RecursiveDirectoryIterator::SKIP_DOTS)
        );
        $base = rtrim($scriptsDir, '/') . '/';
        foreach ($ri as $rf) {
            if (!$rf->isFile() || strtolower($rf->getExtension()) !== 'sh') continue;
            $rel   = ltrim(str_replace($base, '', $rf->getPathname()), '/');
            $parts = explode('/', $rel);
            if (count($parts) < 2 || in_array($parts[0], $exclude)) continue;
            if (in_array($rel, $orchIds) || isset($confMap[$rel])) continue;
            $library[] = ['id' => $rel, 'label' => vv_pretty_label(basename($rel, '.sh'))];
        }
    } catch (Exception $e) {}
    usort($library, fn($a, $b) => strcmp($a['id'], $b['id']));
    return $library;
}

// Load custom-script folder assignments from schedule.json (__folders key).
function vv_folders_load(): array {
    $s = vv_schedule_load();
    $f = $s['__folders'] ?? [];
    return is_array($f) ? $f : [];
}

// Walk the scripts repo and return the job tree.
// Type is derived from the saved cron value: array_start / array_stop → 'event', else 'orchestrator'.
// Well-known event orchs get their cron seeded from $eventDefaults when not yet in schedule.json.
// Any orch or custom script can carry array_start / array_stop as its cron — the event scripts fire all of them.
function vv_job_tree(): array {
    $scriptsDir = SCRIPTS_DIR;
    $schedule   = vv_schedule_load();

    // First-run seeds only — applied when schedule.json has no entry for these IDs yet.
    $eventDefaults = [
        'Orchestrators/array_started.sh'  => 'array_start',
        'Orchestrators/array_stopping.sh' => 'array_stop',
    ];

    $orchs = [];
    foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $path) {
        $id      = 'Orchestrators/' . basename($path);
        $default = $eventDefaults[$id] ?? '';
        $entry   = $schedule[$id] ?? ['enabled' => false, 'cron' => $default];
        $cron    = $entry['cron'] ?? $default;
        $isEvent = str_starts_with($cron, 'array_');
        $suggested = $isEvent ? ['cron' => '', 'label' => ''] : vv_script_suggested_cron($path);
        $orchs[] = [
            'id'              => $id,
            'label'           => vv_pretty_label(basename($path, '.sh')),
            'desc'            => vv_script_description($path),
            'type'            => $isEvent ? 'event' : 'orchestrator',
            'enabled'         => (bool)($entry['enabled'] ?? false),
            'cron'            => $cron,
            'log_enabled'     => (bool)($entry['log_enabled'] ?? false),
            'suggested_cron'  => $suggested['cron'],
            'suggested_label' => $suggested['label'],
            'children'        => vv_script_children($path, $schedule),
            'conf_arrays'     => vv_orch_conf_arrays($path),
        ];
    }

    // Events first (array_start before array_stop), then alphabetical by label.
    usort($orchs, function($a, $b) {
        $ae = $a['type'] === 'event' ? 0 : 1;
        $be = $b['type'] === 'event' ? 0 : 1;
        if ($ae !== $be) return $ae - $be;
        if ($ae === 0) {
            $as = str_contains($a['cron'], 'start') ? 0 : 1;
            $bs = str_contains($b['cron'], 'start') ? 0 : 1;
            if ($as !== $bs) return $as - $bs;


Let me see how the library panel renders and understand all distinct "card" sources in the page:


260
261
262	      <?php
263	      // Build set of IDs that belong to a folder
264	      $_inFolder = [];
265	      foreach ($_folders as $_fn => $_fs) foreach ($_fs as $_fid) $_inFolder[] = $_fid;
266	      ?>
267

268

269	          Custom Scripts
270	          <?= count($customs) ?> script<?= count($customs) !== 1 ? 's' : '' ?>
271	          <button class="vv-advanced-toggle" style="margin-left:auto;width:100px" onclick="vvToggleCustom(this)">Scripts ▸</button>
272

273

274	          <?php if (empty($customs)): ?>
275
No custom scripts yet — click + Add Script to create one.

276	          <?php else: ?>
277
278	            <?php // ── Folder groups ──────────────────────────────────────────────
279	            foreach ($_folders as $_fname => $_fscripts):
280	              $_folderScripts = array_filter($customs, fn($c) => in_array($c['id'], $_fscripts));
281	            ?>
282	            <div class="vv-folder-group" data-folder="<?= htmlspecialchars($_fname) ?>">
283

284	                ▸
285	                <?= htmlspecialchars($_fname) ?>
286	                <?= count($_folderScripts) ?>
287

288

289	                <?php foreach ($_folderScripts as $cs): $csid = htmlspecialchars($cs['id']); ?>
290	                  <div class="vv-script" data-id="<?= $csid ?>">
291

292
293	                        <input type="checkbox" class="vv-enabled" <?= $cs['enabled'] ? 'checked' : '' ?> onchange="vvSaveJob(this)">
294
295
296	                      ⚙<?= htmlspecialchars($cs['label']) ?>
297	                      <input type="text" class="vv-cron" value="<?= htmlspecialchars($cs['cron']) ?>" placeholder="cron expression" onblur="vvSaveCronBlur(this)">
298
299

300	                    <?php if (!empty($cs['desc'])): ?>
301	                      <div class="vv-job-desc" title="<?= htmlspecialchars($cs['desc']) ?>"><?= htmlspecialchars($cs['desc']) ?>

302	                    <?php endif; ?>
303

304	                      <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">▶ Run</button>
305	                      <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ Dry Run</button>
306	                      <button class="vv-btn-sm vv-edit-btn" onclick="vvEditScript('<?= $csid ?>')">Edit</button>
307	                      <?php $csLog = vv_job_log_path($cs['id']); ?>
308	                      <button class="vv-btn-sm vv-log-btn<?= (file_exists($csLog) && filesize($csLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
309	                      <input type="checkbox" class="vv-log-enabled" <?= $cs['log_enabled'] ? 'checked' : '' ?> onchange="vvSaveJob(this)">Verbose
310	                      <input type="text" class="vv-script-args" placeholder="extra args">
311
312

313

314	                <?php endforeach; ?>
315

316

317	            <?php endforeach; ?>
318
319	            <?php // ── Unfoldered scripts ──────────────────────────────────────
320	            foreach ($customs as $cs):
321	              if (in_array($cs['id'], $_inFolder)) continue;
322	              $csid = htmlspecialchars($cs['id']);
323	            ?>
324	              <div class="vv-script" data-id="<?= $csid ?>">
325

326
327	                    <input type="checkbox" class="vv-enabled"
328	                      <?= $cs['enabled'] ? 'checked' : '' ?>
329	                      onchange="vvSaveJob(this)">
330
331
332	                  ⚙<?= htmlspecialchars($cs['label']) ?>
333	                  <input type="text" class="vv-cron" value="<?= htmlspecialchars($cs['cron']) ?>"
334	                    placeholder="cron expression"
335	                    onblur="vvSaveCronBlur(this)">
336
337

338	                <?php if (!empty($cs['desc'])): ?>
339	                  <div class="vv-job-desc" title="<?= htmlspecialchars($cs['desc']) ?>"><?= htmlspecialchars($cs['desc']) ?>

340	                <?php endif; ?>
341

342	                  <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">▶ Run</button>
343	                  <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ Dry Run</button>
344	                  <button class="vv-btn-sm vv-edit-btn" onclick="vvEditScript('<?= $csid ?>')">Edit</button>
345	                  <?php $csLog = vv_job_log_path($cs['id']); ?>
346	                  <button class="vv-btn-sm vv-log-btn<?= (file_exists($csLog) && filesize($csLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
347
348	                    <input type="checkbox" class="vv-log-enabled"
349	                      <?= $cs['log_enabled'] ? 'checked' : '' ?>
350	                      onchange="vvSaveJob(this)">
351	                    Verbose
352
353	                  <input type="text" class="vv-script-args" placeholder="extra args">
354
355

356

357	            <?php endforeach; ?>
358	          <?php endif; ?>
359

360

361
362

363
364

365	        <button class="vv-save-btn vv-add-script-btn" onclick="vvAddScript()">+ Add Script</button>
366	        <button class="vv-save-btn vv-new-folder-btn" onclick="vvNewFolder()" title="Create a folder in Custom Scripts">+ Folder</button>
367	        <button id="vv-arrange-btn" class="vv-save-btn" onclick="vvToggleArrange()" title="Drag scripts between orchestrators">Arrange</button>
368	        <button id="vv-arrange-save-btn" class="vv-save-btn vv-arrange-save-btn" onclick="vvSaveArrange()" style="display:none">Save Arrangement</button>
369	        <button id="vv-arrange-cancel-btn" class="vv-save-btn vv-delete-btn" onclick="vvCancelArrange()" style="display:none">Cancel</button>
370	        <button id="vv-delete-script-btn" class="vv-save-btn vv-delete-btn" onclick="vvDeleteScript()" style="display:none">🗑 Delete</button>
371

372
373

374
375
376

377

378

379

      <?php
      // Build set of IDs that belong to a folder
      $_inFolder = [];
      foreach ($_folders as $_fn => $_fs) foreach ($_fs as $_fid) $_inFolder[] = $_fid;
      ?>

          Custom Scripts
          <?= count($customs) ?> script<?= count($customs) !== 1 ? 's' : '' ?>
          <button class="vv-advanced-toggle" style="margin-left:auto;width:100px" onclick="vvToggleCustom(this)">Scripts ▸</button>

          <?php if (empty($customs)): ?>

No custom scripts yet — click + Add Script to create one.

          <?php else: ?>

            <?php // ── Folder groups ──────────────────────────────────────────────
            foreach ($_folders as $_fname => $_fscripts):
              $_folderScripts = array_filter($customs, fn($c) => in_array($c['id'], $_fscripts));
            ?>
            <div class="vv-folder-group" data-folder="<?= htmlspecialchars($_fname) ?>">

                ▸
                <?= htmlspecialchars($_fname) ?>
                <?= count($_folderScripts) ?>

                <?php foreach ($_folderScripts as $cs): $csid = htmlspecialchars($cs['id']); ?>
                  <div class="vv-script" data-id="<?= $csid ?>">

                        <input type="checkbox" class="vv-enabled" <?= $cs['enabled'] ? 'checked' : '' ?> onchange="vvSaveJob(this)">

                      ⚙<?= htmlspecialchars($cs['label']) ?>
                      <input type="text" class="vv-cron" value="<?= htmlspecialchars($cs['cron']) ?>" placeholder="cron expression" onblur="vvSaveCronBlur(this)">

                    <?php if (!empty($cs['desc'])): ?>
                      <div class="vv-job-desc" title="<?= htmlspecialchars($cs['desc']) ?>"><?= htmlspecialchars($cs['desc']) ?>

                    <?php endif; ?>

                      <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">▶ Run</button>
                      <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ Dry Run</button>
                      <button class="vv-btn-sm vv-edit-btn" onclick="vvEditScript('<?= $csid ?>')">Edit</button>
                      <?php $csLog = vv_job_log_path($cs['id']); ?>
                      <button class="vv-btn-sm vv-log-btn<?= (file_exists($csLog) && filesize($csLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
                      <input type="checkbox" class="vv-log-enabled" <?= $cs['log_enabled'] ? 'checked' : '' ?> onchange="vvSaveJob(this)">Verbose
                      <input type="text" class="vv-script-args" placeholder="extra args">

                <?php endforeach; ?>

            <?php endforeach; ?>

            <?php // ── Unfoldered scripts ──────────────────────────────────────
            foreach ($customs as $cs):
              if (in_array($cs['id'], $_inFolder)) continue;
              $csid = htmlspecialchars($cs['id']);
            ?>
              <div class="vv-script" data-id="<?= $csid ?>">

                    <input type="checkbox" class="vv-enabled"
                      <?= $cs['enabled'] ? 'checked' : '' ?>
                      onchange="vvSaveJob(this)">

                  ⚙<?= htmlspecialchars($cs['label']) ?>
                  <input type="text" class="vv-cron" value="<?= htmlspecialchars($cs['cron']) ?>"
                    placeholder="cron expression"
                    onblur="vvSaveCronBlur(this)">

                <?php if (!empty($cs['desc'])): ?>
                  <div class="vv-job-desc" title="<?= htmlspecialchars($cs['desc']) ?>"><?= htmlspecialchars($cs['desc']) ?>

                <?php endif; ?>

                  <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">▶ Run</button>
                  <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ Dry Run</button>
                  <button class="vv-btn-sm vv-edit-btn" onclick="vvEditScript('<?= $csid ?>')">Edit</button>
                  <?php $csLog = vv_job_log_path($cs['id']); ?>
                  <button class="vv-btn-sm vv-log-btn<?= (file_exists($csLog) && filesize($csLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>

                    <input type="checkbox" class="vv-log-enabled"
                      <?= $cs['log_enabled'] ? 'checked' : '' ?>
                      onchange="vvSaveJob(this)">
                    Verbose

                  <input type="text" class="vv-script-args" placeholder="extra args">

            <?php endforeach; ?>
          <?php endif; ?>

        <button class="vv-save-btn vv-add-script-btn" onclick="vvAddScript()">+ Add Script</button>
        <button class="vv-save-btn vv-new-folder-btn" onclick="vvNewFolder()" title="Create a folder in Custom Scripts">+ Folder</button>
        <button id="vv-arrange-btn" class="vv-save-btn" onclick="vvToggleArrange()" title="Drag scripts between orchestrators">Arrange</button>
        <button id="vv-arrange-save-btn" class="vv-save-btn vv-arrange-save-btn" onclick="vvSaveArrange()" style="display:none">Save Arrangement</button>
        <button id="vv-arrange-cancel-btn" class="vv-save-btn vv-delete-btn" onclick="vvCancelArrange()" style="display:none">Cancel</button>
        <button id="vv-delete-script-btn" class="vv-save-btn vv-delete-btn" onclick="vvDeleteScript()" style="display:none">🗑 Delete</button>


grep -n "_library\|library" /boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php | head -20

Find where library is rendered in scheduler page


17:$_library = vv_script_library();
962:          <div id="vv-library-drop-zone" class="vv-library-zone"
967:

968:              <?php foreach ($_library as $_lib): ?>
978:              <?php if (empty($_library)): ?>
3578:let vvDragFromArray  = null;  // null = unassigned/library
3856:// Move a library card into an orch (dragging from unassigned pool)
3875:  const ph = document.getElementById('vv-library-cards').querySelector('.vv-board-placeholder');
3882:      const libCards = document.getElementById('vv-library-cards');
3897:// Drop on library zone → remove from orch
3904:  if (!script || !fromArray || !srcEl) return;  // library→library or custom script noop
3912:  const libCards = document.getElementById('vv-library-cards');


431	function vv_conf_script_map(): array {
432	    static $cache = null;
433	    if ($cache !== null) return $cache;
434	    $confPath = CONF_DIR . '/master.conf';
435	    if (!file_exists($confPath)) return $cache = [];
436	    $lines    = file($confPath, FILE_IGNORE_NEW_LINES) ?: [];
437	    $map      = [];
438	    $inArray  = false;
439	    $arrayVar = '';
440	    foreach ($lines as $line) {
441	        if (preg_match('/^\s*([A-Z_]+_SCRIPTS)\s*=\s*\(/', $line, $am)) { $inArray = true;  $arrayVar = $am[1]; }
442	        if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
443	        if (!$inArray) continue;
444	        if (!preg_match('/^\s*(#\s*)?"([^"]+)"/', $line, $e)) continue;
445	        $commented = trim($e[1]) !== '';
446	        $parts     = preg_split('/\s+/', trim($e[2]));
447	        $path      = $parts[0] ?? '';
448	        if (substr($path, -3) !== '.sh') continue;
449	        if (!isset($map[$path])) $map[$path] = ['array' => $arrayVar, 'enabled' => !$commented, 'managed' => true];
450	    }
451	    return $cache = $map;
452	}
453
454	// Read a boolean flag value (e.g. INTERMEDIATE_RSYNC_ENABLED) from master.conf.
455	function vv_conf_flag_value(string $name): bool {
456	    $conf = file_get_contents(CONF_DIR . '/master.conf') ?: '';
457	    if (preg_match('/^\s*' . preg_quote($name, '/') . '\s*=\s*(true|false)\s*$/m', $conf, $m)) {
458	        return $m[1] === 'true';
459	    }
460	    return false;
461	}
462
463	// Write a boolean flag value to master.conf.
464	function vv_conf_flag_set(string $name, bool $value): bool {
465	    $confPath = CONF_DIR . '/master.conf';
466	    $content  = file_get_contents($confPath);
467	    if ($content === false) return false;
468	    $val      = $value ? 'true' : 'false';
469	    $new      = preg_replace(
470	        '/^(\s*' . preg_quote($name, '/') . '\s*=\s*)(true|false)(\s*(?:#.*)?)$/m',
471	        '${1}' . $val . '${3}',
472	        $content, -1, $count
473	    );
474	    if (!$count) return false;
475	    return file_put_contents($confPath, $new) !== false;
476	}
477
478	// Comment or uncomment a script's line in the first master.conf array that contains it.
479	function vv_conf_toggle_script(string $rel, bool $enable): bool {
480	    $confPath = CONF_DIR . '/master.conf';
481	    $lines    = file($confPath, FILE_KEEP_BLANK_LINES);
482	    if (!$lines) return false;
483	    $changed  = false;
484	    $inArray  = false;
485	    $relEsc   = preg_quote($rel, '/');
486	    foreach ($lines as &$line) {
487	        if (preg_match('/^\s*[A-Z_]+_SCRIPTS\s*=\s*\(/', $line)) $inArray = true;
488	        if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
489	        if (!$inArray) continue;
490	        if (!preg_match('/^\s*(?:#\s*)?"' . $relEsc . '(?:\s[^"]*)?"/', $line)) continue;

function vv_conf_script_map(): array {
    static $cache = null;
    if ($cache !== null) return $cache;
    $confPath = CONF_DIR . '/master.conf';
    if (!file_exists($confPath)) return $cache = [];
    $lines    = file($confPath, FILE_IGNORE_NEW_LINES) ?: [];
    $map      = [];
    $inArray  = false;
    $arrayVar = '';
    foreach ($lines as $line) {
        if (preg_match('/^\s*([A-Z_]+_SCRIPTS)\s*=\s*\(/', $line, $am)) { $inArray = true;  $arrayVar = $am[1]; }
        if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
        if (!$inArray) continue;
        if (!preg_match('/^\s*(#\s*)?"([^"]+)"/', $line, $e)) continue;
        $commented = trim($e[1]) !== '';
        $parts     = preg_split('/\s+/', trim($e[2]));
        $path      = $parts[0] ?? '';
        if (substr($path, -3) !== '.sh') continue;
        if (!isset($map[$path])) $map[$path] = ['array' => $arrayVar, 'enabled' => !$commented, 'managed' => true];
    }
    return $cache = $map;
}

// Read a boolean flag value (e.g. INTERMEDIATE_RSYNC_ENABLED) from master.conf.
function vv_conf_flag_value(string $name): bool {
    $conf = file_get_contents(CONF_DIR . '/master.conf') ?: '';
    if (preg_match('/^\s*' . preg_quote($name, '/') . '\s*=\s*(true|false)\s*$/m', $conf, $m)) {
        return $m[1] === 'true';
    }
    return false;
}

// Write a boolean flag value to master.conf.
function vv_conf_flag_set(string $name, bool $value): bool {
    $confPath = CONF_DIR . '/master.conf';
    $content  = file_get_contents($confPath);
    if ($content === false) return false;
    $val      = $value ? 'true' : 'false';
    $new      = preg_replace(
        '/^(\s*' . preg_quote($name, '/') . '\s*=\s*)(true|false)(\s*(?:#.*)?)$/m',
        '${1}' . $val . '${3}',
        $content, -1, $count
    );
    if (!$count) return false;
    return file_put_contents($confPath, $new) !== false;
}

// Comment or uncomment a script's line in the first master.conf array that contains it.
function vv_conf_toggle_script(string $rel, bool $enable): bool {
    $confPath = CONF_DIR . '/master.conf';
    $lines    = file($confPath, FILE_KEEP_BLANK_LINES);
    if (!$lines) return false;
    $changed  = false;
    $inArray  = false;
    $relEsc   = preg_quote($rel, '/');
    foreach ($lines as &$line) {
        if (preg_match('/^\s*[A-Z_]+_SCRIPTS\s*=\s*\(/', $line)) $inArray = true;
        if ($inArray && preg_match('/^\s*\)\s*(?:#.*)?$/', $line) && !str_contains($line, '(')) $inArray = false;
        if (!$inArray) continue;
        if (!preg_match('/^\s*(?:#\s*)?"' . $relEsc . '(?:\s[^"]*)?"/', $line)) continue;


Now I have the full picture. The cards that scan folders are:
- **Tools** — already done
- **Custom Scripts** — needs `Plugin/*/Custom/` merge
- **Script library** (the Arrange panel) — currently has `Plugin` in its hard-exclude list, so all adapter scripts are invisible there

Let me read the exact current `vv_custom_scripts` and library bodies before editing:


270	function vv_custom_scripts(): array {
271	    $dir      = SCRIPTS_DIR . '/Custom';
272	    $schedule = vv_schedule_load();
273	    $scripts  = [];
274	    foreach (glob("$dir/*.sh") ?: [] as $path) {
275	        $rel       = 'Custom/' . basename($path);
276	        $entry     = $schedule[$rel] ?? [];
277	        $scripts[] = [
278	            'id'          => $rel,
279	            'label'       => vv_pretty_label(basename($path, '.sh')),
280	            'desc'        => vv_script_description($path),
281	            'enabled'     => (bool)($entry['enabled'] ?? false),
282	            'cron'        => $entry['cron'] ?? '',
283	            'log_enabled' => (bool)($entry['log_enabled'] ?? false),
284	        ];
285	    }
286	    return $scripts;
287	}
288
289	// Load rsync standalone config (location + cron) for a flag name from schedule.json.
290	function vv_rsync_standalone(string $flagName): array {
291	    $s = vv_schedule_load();
292	    $r = $s['__rsync_' . $flagName] ?? [];
293	    return [
294	        'location' => (string)($r['location'] ?? ''),
295	        'cron'     => (string)($r['cron']     ?? ''),
296	    ];
297	}
298
299	// Extract *_SCRIPTS array variable names that an orchestrator iterates over.
300	function vv_orch_conf_arrays(string $orchPath): array {
301	    $content = file_get_contents($orchPath) ?: '';
302	    preg_match_all('/\$\{([A-Z_]+_SCRIPTS)\[@\]\}/', $content, $refs);
303	    return array_unique($refs[1] ?? []);
304	}
305
306	// Return .sh scripts that exist in SCRIPTS_DIR but are not referenced in any
307	// master.conf *_SCRIPTS array and are not orchestrators or custom scripts.
308	function vv_script_library(): array {
309	    $scriptsDir = SCRIPTS_DIR;
310	    $confMap    = vv_conf_script_map();
311	    $orchIds    = [];
312	    foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $p) {
313	        $orchIds[] = 'Orchestrators/' . basename($p);
314	    }

function vv_custom_scripts(): array {
    $dir      = SCRIPTS_DIR . '/Custom';
    $schedule = vv_schedule_load();
    $scripts  = [];
    foreach (glob("$dir/*.sh") ?: [] as $path) {
        $rel       = 'Custom/' . basename($path);
        $entry     = $schedule[$rel] ?? [];
        $scripts[] = [
            'id'          => $rel,
            'label'       => vv_pretty_label(basename($path, '.sh')),
            'desc'        => vv_script_description($path),
            'enabled'     => (bool)($entry['enabled'] ?? false),
            'cron'        => $entry['cron'] ?? '',
            'log_enabled' => (bool)($entry['log_enabled'] ?? false),
        ];
    }
    return $scripts;
}

// Load rsync standalone config (location + cron) for a flag name from schedule.json.
function vv_rsync_standalone(string $flagName): array {
    $s = vv_schedule_load();
    $r = $s['__rsync_' . $flagName] ?? [];
    return [
        'location' => (string)($r['location'] ?? ''),
        'cron'     => (string)($r['cron']     ?? ''),
    ];
}

// Extract *_SCRIPTS array variable names that an orchestrator iterates over.
function vv_orch_conf_arrays(string $orchPath): array {
    $content = file_get_contents($orchPath) ?: '';
    preg_match_all('/\$\{([A-Z_]+_SCRIPTS)\[@\]\}/', $content, $refs);
    return array_unique($refs[1] ?? []);
}

// Return .sh scripts that exist in SCRIPTS_DIR but are not referenced in any
// master.conf *_SCRIPTS array and are not orchestrators or custom scripts.
function vv_script_library(): array {
    $scriptsDir = SCRIPTS_DIR;
    $confMap    = vv_conf_script_map();
    $orchIds    = [];
    foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $p) {
        $orchIds[] = 'Orchestrators/' . basename($p);
    }


Now I'll update both functions:


Edit

function vv_custom_scripts(): array {
    $dir      = SCRIPTS_DIR . '/Custom';
    $schedule = vv_schedule_load();
    $scripts  = [];
    foreach (glob("$dir/*.sh") ?: [] as $path) {
        $rel       = 'Custom/' . basename($path);
        $entry     = $schedule[$rel] ?? [];
        $scripts[] = [
            'id'          => $rel,
            'label'       => vv_pretty_label(basename($path, '.sh')),
            'desc'        => vv_script_description($path),
            'enabled'     => (bool)($entry['enabled'] ?? false),
            'cron'        => $entry['cron'] ?? '',
            'log_enabled' => (bool)($entry['log_enabled'] ?? false),
        ];
    }
    return $scripts;
}

function vv_custom_scripts(): array {
    $schedule = vv_schedule_load();
    $scripts  = [];

    $collect = function(string $dir, string $relPrefix) use ($schedule, &$scripts): void {
        foreach (glob("$dir/*.sh") ?: [] as $path) {
            $rel       = $relPrefix . basename($path);
            $entry     = $schedule[$rel] ?? [];
            $scripts[] = [
                'id'          => $rel,
                'label'       => vv_pretty_label(basename($path, '.sh')),
                'desc'        => vv_script_description($path),
                'enabled'     => (bool)($entry['enabled'] ?? false),
                'cron'        => $entry['cron'] ?? '',
                'log_enabled' => (bool)($entry['log_enabled'] ?? false),
            ];
        }
    };

    $collect(SCRIPTS_DIR . '/Custom', 'Custom/');

    // Platform adapter custom scripts (Plugin/<platform>/Custom/)
    foreach (glob(SCRIPTS_DIR . '/Plugin/*/Custom') ?: [] as $customDir) {
        $platform = basename(dirname($customDir));
        $collect($customDir, "Plugin/$platform/Custom/");
    }

    return $scripts;
}


The file /boot/config/plugins/varaverk/Plugin/unraid/include/scheduler.php has been updated successfully. (file state is current in your context — no need to Read it back)

function vv_custom_scripts(): array {
    $dir      = SCRIPTS_DIR . '/Custom';
    $schedule = vv_schedule_load();
    $scripts  = [];
    foreach (glob("$dir/*.sh") ?: [] as $path) {
        $rel       = 'Custom/' . basename($path);
        $entry     = $schedule[$rel] ?? [];
        $scripts[] = [
            'id'          => $rel,
            'label'       => vv_pretty_label(basename($path, '.sh')),
            'desc'        => vv_script_description($path),
            'enabled'     => (bool)($entry['enabled'] ?? false),
            'cron'        => $entry['cron'] ?? '',
            'log_enabled' => (bool)($entry['log_enabled'] ?? false),
        ];
    }
    return $scripts;
}

function vv_custom_scripts(): array {
    $schedule = vv_schedule_load();
    $scripts  = [];

    $collect = function(string $dir, string $relPrefix) use ($schedule, &$scripts): void {
        foreach (glob("$dir/*.sh") ?: [] as $path) {
            $rel       = $relPrefix . basename($path);
            $entry     = $schedule[$rel] ?? [];
            $scripts[] = [
                'id'          => $rel,
                'label'       => vv_pretty_label(basename($path, '.sh')),
                'desc'        => vv_script_description($path),
                'enabled'     => (bool)($entry['enabled'] ?? false),
                'cron'        => $entry['cron'] ?? '',
                'log_enabled' => (bool)($entry['log_enabled'] ?? false),
            ];
        }
    };

    $collect(SCRIPTS_DIR . '/Custom', 'Custom/');

    // Platform adapter custom scripts (Plugin/<platform>/Custom/)
    foreach (glob(SCRIPTS_DIR . '/Plugin/*/Custom') ?: [] as $customDir) {
        $platform = basename(dirname($customDir));
        $collect($customDir, "Plugin/$platform/Custom/");
    }

    return $scripts;
}

-    $dir      = SCRIPTS_DIR . '/Custom';

-    foreach (glob("$dir/*.sh") ?: [] as $path) {

-        $rel       = 'Custom/' . basename($path);

-        $entry     = $schedule[$rel] ?? [];

-            'label'       => vv_pretty_label(basename($path, '.sh')),

-            'desc'        => vv_script_description($path),

-            'enabled'     => (bool)($entry['enabled'] ?? false),

-            'log_enabled' => (bool)($entry['log_enabled'] ?? false),

+    $collect = function(string $dir, string $relPrefix) use ($schedule, &$scripts): void {

+        foreach (glob("$dir/*.sh") ?: [] as $path) {

+            $rel       = $relPrefix . basename($path);

+            $entry     = $schedule[$rel] ?? [];

+                'label'       => vv_pretty_label(basename($path, '.sh')),

+                'desc'        => vv_script_description($path),

+                'enabled'     => (bool)($entry['enabled'] ?? false),

+                'log_enabled' => (bool)($entry['log_enabled'] ?? false),

+    $collect(SCRIPTS_DIR . '/Custom', 'Custom/');

+    // Platform adapter custom scripts (Plugin/<platform>/Custom/)

+    foreach (glob(SCRIPTS_DIR . '/Plugin/*/Custom') ?: [] as $customDir) {

+        $platform = basename(dirname($customDir));

+        $collect($customDir, "Plugin/$platform/Custom/");


Now update the library — remove `Plugin` from the hard-exclude, add adapter-aware logic, and exclude scripts already shown in cards:


// Return .sh scripts that exist in SCRIPTS_DIR but are not referenced in any
// master.conf *_SCRIPTS array and are not orchestrators or custom scripts.
function vv_script_library(): array {
    $scriptsDir = SCRIPTS_DIR;
    $confMap    = vv_conf_script_map();
    $orchIds    = [];
    foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $p) {
        $orchIds[] = 'Orchestrators/' . basename($p);
    }
    $exclude = ['Plugin', '.git', 'Orchestrators', 'Custom', 'Configurations'];
    $library = [];
    try {
        $ri   = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($scriptsDir, RecursiveDirectoryIterator::SKIP_DOTS)
        );
        $base = rtrim($scriptsDir, '/') . '/';
        foreach ($ri as $rf) {
            if (!$rf->isFile() || strtolower($rf->getExtension()) !== 'sh') continue;
            $rel   = ltrim(str_replace($base, '', $rf->getPathname()), '/');
            $parts = explode('/', $rel);
            if (count($parts) < 2 || in_array($parts[0], $exclude)) continue;
            if (in_array($rel, $orchIds) || isset($confMap[$rel])) continue;
            $library[] = ['id' => $rel, 'label' => vv_pretty_label(basename($rel, '.sh'))];
        }
    } catch (Exception $e) {}
    usort($library, fn($a, $b) => strcmp($a['id'], $b['id']));
    return $library;
}

// Return .sh scripts that exist in SCRIPTS_DIR but are not referenced in any
// master.conf *_SCRIPTS array and are not shown in any other scheduler card.
function vv_script_library(): array {
    $scriptsDir = SCRIPTS_DIR;
    $confMap    = vv_conf_script_map();
    $orchIds    = [];
    foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $p) {
        $orchIds[] = 'Orchestrators/' . basename($p);
    }
    // Scripts already shown in their own cards are not "unlisted"
    $cardIds = array_flip(array_merge(
        array_column(vv_tools_scripts(),  'id'),
        array_column(vv_custom_scripts(), 'id')
    ));

    // UI-only subdirs under Plugin/<platform>/ — no runnable scripts
    $pluginUiDirs = ['api', 'include', 'pages', 'css', 'js', 'icons', 'event'];

    $exclude = ['.git', 'Orchestrators', 'Custom', 'Configurations'];
    $library = [];
    try {
        $ri   = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($scriptsDir, RecursiveDirectoryIterator::SKIP_DOTS)
        );
        $base = rtrim($scriptsDir, '/') . '/';
        foreach ($ri as $rf) {
            if (!$rf->isFile() || strtolower($rf->getExtension()) !== 'sh') continue;
            $rel   = ltrim(str_replace($base, '', $rf->getPathname()), '/');
            $parts = explode('/', $rel);
            if (in_array($parts[0], $exclude)) continue;
            if ($parts[0] === 'Plugin') {
                // Require Plugin/<platform>/<category>/<script>.sh — skip root-level adapter files
                if (count($parts) < 4) continue;
                // Skip UI-only category dirs
                if (in_array($parts[2], $pluginUiDirs)) continue;
            } elseif (count($parts) < 2) {
                continue;
            }
            if (in_array($rel, $orchIds) || isset($confMap[$rel]) || isset($cardIds[$rel])) continue;
            $library[] = ['id' => $rel, 'label' => vv_pretty_label(basename($rel, '.sh'))];
        }
    } catch (Exception $e) {}
    usort($library, fn($a, $b) => strcmp($a['id'], $b['id']));
    return $library;
}


// Return .sh scripts that exist in SCRIPTS_DIR but are not referenced in any
// master.conf *_SCRIPTS array and are not orchestrators or custom scripts.
function vv_script_library(): array {
    $scriptsDir = SCRIPTS_DIR;
    $confMap    = vv_conf_script_map();
    $orchIds    = [];
    foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $p) {
        $orchIds[] = 'Orchestrators/' . basename($p);
    }
    $exclude = ['Plugin', '.git', 'Orchestrators', 'Custom', 'Configurations'];
    $library = [];
    try {
        $ri   = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($scriptsDir, RecursiveDirectoryIterator::SKIP_DOTS)
        );
        $base = rtrim($scriptsDir, '/') . '/';
        foreach ($ri as $rf) {
            if (!$rf->isFile() || strtolower($rf->getExtension()) !== 'sh') continue;
            $rel   = ltrim(str_replace($base, '', $rf->getPathname()), '/');
            $parts = explode('/', $rel);
            if (count($parts) < 2 || in_array($parts[0], $exclude)) continue;
            if (in_array($rel, $orchIds) || isset($confMap[$rel])) continue;
            $library[] = ['id' => $rel, 'label' => vv_pretty_label(basename($rel, '.sh'))];
        }
    } catch (Exception $e) {}
    usort($library, fn($a, $b) => strcmp($a['id'], $b['id']));
    return $library;
}

// Return .sh scripts that exist in SCRIPTS_DIR but are not referenced in any
// master.conf *_SCRIPTS array and are not shown in any other scheduler card.
function vv_script_library(): array {
    $scriptsDir = SCRIPTS_DIR;
    $confMap    = vv_conf_script_map();
    $orchIds    = [];
    foreach (glob("$scriptsDir/Orchestrators/*.sh") ?: [] as $p) {
        $orchIds[] = 'Orchestrators/' . basename($p);
    }
    // Scripts already shown in their own cards are not "unlisted"
    $cardIds = array_flip(array_merge(
        array_column(vv_tools_scripts(),  'id'),
        array_column(vv_custom_scripts(), 'id')
    ));

    // UI-only subdirs under Plugin/<platform>/ — no runnable scripts
    $pluginUiDirs = ['api', 'include', 'pages', 'css', 'js', 'icons', 'event'];

    $exclude = ['.git', 'Orchestrators', 'Custom', 'Configurations'];
    $library = [];
    try {
        $ri   = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($scriptsDir, RecursiveDirectoryIterator::SKIP_DOTS)
        );
        $base = rtrim($scriptsDir, '/') . '/';
        foreach ($ri as $rf) {
            if (!$rf->isFile() || strtolower($rf->getExtension()) !== 'sh') continue;
            $rel   = ltrim(str_replace($base, '', $rf->getPathname()), '/');
            $parts = explode('/', $rel);
            if (in_array($parts[0], $exclude)) continue;
            if ($parts[0] === 'Plugin') {
                // Require Plugin/<platform>/<category>/<script>.sh — skip root-level adapter files
                if (count($parts) < 4) continue;
                // Skip UI-only category dirs
                if (in_array($parts[2], $pluginUiDirs)) continue;
            } elseif (count($parts) < 2) {
                continue;
            }
            if (in_array($rel, $orchIds) || isset($confMap[$rel]) || isset($cardIds[$rel])) continue;
            $library[] = ['id' => $rel, 'label' => vv_pretty_label(basename($rel, '.sh'))];
        }
    } catch (Exception $e) {}
    usort($library, fn($a, $b) => strcmp($a['id'], $b['id']));
    return $library;
}

-// master.conf *_SCRIPTS array and are not orchestrators or custom scripts.

+// master.conf *_SCRIPTS array and are not shown in any other scheduler card.

-    $exclude = ['Plugin', '.git', 'Orchestrators', 'Custom', 'Configurations'];

+    // Scripts already shown in their own cards are not "unlisted"

+        array_column(vv_tools_scripts(),  'id'),

+        array_column(vv_custom_scripts(), 'id')

+    // UI-only subdirs under Plugin/<platform>/ — no runnable scripts

+    $pluginUiDirs = ['api', 'include', 'pages', 'css', 'js', 'icons', 'event'];

+    $exclude = ['.git', 'Orchestrators', 'Custom', 'Configurations'];

-            if (count($parts) < 2 || in_array($parts[0], $exclude)) continue;

-            if (in_array($rel, $orchIds) || isset($confMap[$rel])) continue;

+            if (in_array($parts[0], $exclude)) continue;

+            if ($parts[0] === 'Plugin') {

+                // Require Plugin/<platform>/<category>/<script>.sh — skip root-level adapter files

+                if (count($parts) < 4) continue;

+                // Skip UI-only category dirs

+                if (in_array($parts[2], $pluginUiDirs)) continue;

+            } elseif (count($parts) < 2) {

+            if (in_array($rel, $orchIds) || isset($confMap[$rel]) || isset($cardIds[$rel])) continue;


Now verify all three functions work correctly:


Bash

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/scheduler.php';

echo '=== Tools (' . count(\$t = vv_tools_scripts()) . ') ===\n';
foreach (\$t as \$x) echo '  ' . \$x['id'] . '\n';

echo '\n=== Custom (' . count(\$c = vv_custom_scripts()) . ') ===\n';
foreach (\$c as \$x) echo '  ' . \$x['id'] . '\n';
if (empty(\$c)) echo '  (none — Custom/ dir empty)\n';

echo '\n=== Library (spot check Plugin/ items) ===\n';
\$lib = vv_script_library();
\$pluginItems = array_filter(\$lib, fn(\$x) => str_starts_with(\$x['id'], 'Plugin/'));
foreach (\$pluginItems as \$x) echo '  PLUGIN: ' . \$x['id'] . '\n';
if (empty(\$pluginItems)) echo '  (no Plugin/ scripts in library — all properly accounted for in cards/confMap)\n';
echo '  Total library: ' . count(\$lib) . ' scripts\n';
" 2>&1

Verify all three card functions work correctly


=== Tools (16) ===\n  Tools/bulk_permissions_repair.sh\n  Tools/claude_startup.sh\n  Plugin/unraid/Tools/conf_populate.sh\n  Tools/container_data_export.sh\n  Tools/docker_prune_images.sh\n  Tools/emby_database_repair.sh\n  Tools/emby_to_lidarr_sync.sh\n  Tools/emby_to_radarr_sync.sh\n  Tools/emby_to_sonarr_sync.sh\n  Tools/fallback_state_reset.sh\n  Tools/ramdisk_stop.sh\n  Plugin/unraid/Tools/recreate_shares.sh\n  Tools/smart_long_test.sh\n  Plugin/unraid/Tools/storage_migrate.sh\n  Tools/watchdog_skip_list_manager.sh\n  Tools/zfs_pool_scrub.sh\n\n=== Custom (0) ===\n  (none — Custom/ dir empty)\n\n=== Library (spot check Plugin/ items) ===\n  PLUGIN: Plugin/unraid/Tools/api_cache_writer.sh\n  PLUGIN: Plugin/unraid/Tools/remote_arr_cache_writer.sh\n  Total library: 22 scripts\n


The two background writers slip through since they're scheduled standalone but not in any card. The fix is to also exclude any script with an existing schedule.json entry:


Edit

    // Scripts already shown in their own cards are not "unlisted"
    $cardIds = array_flip(array_merge(
        array_column(vv_tools_scripts(),  'id'),
        array_column(vv_custom_scripts(), 'id')
    ));

    // Scripts already shown in their own cards are not "unlisted"
    $schedule = vv_schedule_load();
    $cardIds  = array_flip(array_merge(
        array_column(vv_tools_scripts(),  'id'),
        array_column(vv_custom_scripts(), 'id')
    ));


    // Scripts already shown in their own cards are not "unlisted"
    $cardIds = array_flip(array_merge(
        array_column(vv_tools_scripts(),  'id'),
        array_column(vv_custom_scripts(), 'id')
    ));

    // Scripts already shown in their own cards are not "unlisted"
    $schedule = vv_schedule_load();
    $cardIds  = array_flip(array_merge(
        array_column(vv_tools_scripts(),  'id'),
        array_column(vv_custom_scripts(), 'id')
    ));

$orchIds[] = 'Orchestrators/' . basename($p);

+    $cardIds  = array_flip(array_merge(


if (in_array($rel, $orchIds) || isset($confMap[$rel]) || isset($cardIds[$rel])) continue;

if (in_array($rel, $orchIds) || isset($confMap[$rel]) || isset($cardIds[$rel]) || isset($schedule[$rel])) continue;


-            if (in_array($rel, $orchIds) || isset($confMap[$rel]) || isset($cardIds[$rel])) continue;

+            if (in_array($rel, $orchIds) || isset($confMap[$rel]) || isset($cardIds[$rel]) || isset($schedule[$rel])) continue;


Bash

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/scheduler.php';

\$lib = vv_script_library();
\$pluginItems = array_filter(\$lib, fn(\$x) => str_starts_with(\$x['id'], 'Plugin/'));
echo '=== Plugin/ items in library ===\n';
foreach (\$pluginItems as \$x) echo '  ' . \$x['id'] . '\n';
if (empty(\$pluginItems)) echo '  (none — all accounted for)\n';
echo 'Total library: ' . count(\$lib) . '\n';
" 2>&1

Verify no Plugin/ scripts leak into library


=== Plugin/ items in library ===\n  (none — all accounted for)\nTotal library: 20\n


Clean. Here's what was updated across all three card functions:

**`vv_tools_scripts()`** — already done. Scans `Tools/` + `Plugin/*/Tools/`, excludes background writers.

**`vv_custom_scripts()`** — same pattern applied. Scans `Custom/` + `Plugin/*/Custom/`. When you add Unraid-specific custom scripts under `Plugin/unraid/Custom/`, they appear in the Custom card automatically.

**`vv_script_library()`** (Arrange panel):
- Removed `Plugin` from the hard-exclude
- Plugin scripts require depth ≥ 4 (`Plugin/<platform>/<category>/script.sh`) — adapter root files (`adapter.sh`, `run_job.sh`, `user_script_plug-in.sh`) stay hidden
- UI-only subdirs (`api`, `include`, `pages`, `css`, `js`, `icons`, `event`) explicitly skipped
- Added card ID exclusion and schedule.json exclusion — so the background writers and anything explicitly scheduled disappears from the library; only truly unattached scripts appear

Any new `Plugin/<platform>/<category>/` bash script now flows naturally to whichever card matches by folder name, or falls into the library if it has no home yet.


api cache writer7m ago
bash: /boot/config/plugins/varaverk/Plugin/unraid/tools/api_cache_writer.sh: No such file or directory


The cron is still pointing at the old lowercase path. Let me check what's invoking it:


cat /boot/config/plugins/varaverk/varaverk.cron 2>/dev/null | grep -i "api_cache\|cache_writer"
echo "---"
grep -rn "api_cache_writer\|tools/api" /boot/config/plugins/varaverk \
  --include="*.sh" --include="*.php" --include="*.cron" --include="*.json" \
  | grep -v "Plugin/unraid/Tools/"

Find all references to api_cache_writer still using old path


* * * * * bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Plugin/unraid/Tools/api_cache_writer.sh" "/boot/config/plugins/varaverk/Plugin/unraid/Tools/api_cache_writer.sh"
0 */2 * * * bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Plugin/unraid/Tools/remote_arr_cache_writer.sh" "/boot/config/plugins/varaverk/Plugin/unraid/Tools/remote_arr_cache_writer.sh"
---
/boot/config/plugins/varaverk/Plugin/unraid/include/scheduler.php:235:    static $EXCLUDE = ['api_cache_writer.sh', 'remote_arr_cache_writer.sh'];


api cache writer7m ago bash: /boot/config/plugins/varaverk/Plugin/unraid/tools/api_cache_writer.sh: No such file or directory


crontab -l 2>/dev/null | grep -i "api_cache\|varaverk"
echo "---"
cat /etc/cron.d/varaverk 2>/dev/null | grep -i "api_cache\|cache_writer"
echo "---"
ls /var/spool/cron/crontabs/ 2>/dev/null

Check installed system cron for old path


---
---
root


crontab -l -u root 2>/dev/null | grep -i "api_cache\|varaverk\|run_job" | head -20

Check root crontab for old varaverk entries


(Bash completed with no output)


cat /var/spool/cron/crontabs/root 2>/dev/null | grep -i "api_cache\|run_job\|varaverk" | head -20

Read root crontab file directly


grep -r "api_cache_writer\|run_job" /var/spool/cron/ 2>/dev/null
echo "---"
# Check where update_cron installs to
grep -n "update_cron\|crontab\|cron.d\|install_cron" \
  /boot/config/plugins/varaverk/Plugin/unraid/include/scheduler.php | head -20

Find where cron is actually installed


---
9:define('CRON_FILE',     '/boot/config/plugins/varaverk/varaverk.cron'); // must stay in /boot — update_cron scans there
124:    // Write to the plugin cron file; update_cron merges all plugin *.cron files into /etc/cron.d/root.
126:    exec('/usr/local/sbin/update_cron');
127:    // Remove legacy direct cron file left from before update_cron migration — prevents duplicate job firing.
128:    @unlink('/etc/cron.d/varaverk');


grep -i "api_cache\|run_job\|Plugin/unraid/tools" /etc/cron.d/root 2>/dev/null | head -20

Check /etc/cron.d/root for old path


*/30 * * * * bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Orchestrators/critical_sync_maintenance.sh" "/boot/config/plugins/varaverk/Orchestrators/critical_sync_maintenance.sh"
0 1 * * * bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Orchestrators/daily_sync_maintenance.sh" "/boot/config/plugins/varaverk/Orchestrators/daily_sync_maintenance.sh"
0 */4 * * * bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Orchestrators/intermediate_sync_maintenance.sh" "/boot/config/plugins/varaverk/Orchestrators/intermediate_sync_maintenance.sh"
0 0 15 * * bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Orchestrators/monthly_maintenance.sh" "/boot/config/plugins/varaverk/Orchestrators/monthly_maintenance.sh"
0 7 * * 0 bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Orchestrators/sunday_morning_coffee_report.sh" "/boot/config/plugins/varaverk/Orchestrators/sunday_morning_coffee_report.sh"
*/7 * * * * bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Orchestrators/transcode_management.sh" "/boot/config/plugins/varaverk/Orchestrators/transcode_management.sh"
*/15 * * * * bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Orchestrators/watchdog_orchestrator.sh" "/boot/config/plugins/varaverk/Orchestrators/watchdog_orchestrator.sh"
30 2 * * 0 bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Orchestrators/weekly_sync_maintenance.sh" "/boot/config/plugins/varaverk/Orchestrators/weekly_sync_maintenance.sh"
* * * * * bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Plugin/unraid/Tools/api_cache_writer.sh" "/boot/config/plugins/varaverk/Plugin/unraid/Tools/api_cache_writer.sh"
0 */2 * * * bash "/boot/config/plugins/varaverk/Plugin/unraid/run_job.sh" "Plugin/unraid/Tools/remote_arr_cache_writer.sh" "/boot/config/plugins/varaverk/Plugin/unraid/Tools/remote_arr_cache_writer.sh"


The cron is correct now — that error is from the stale cron entry that fired before the rebuild. Let me verify it won't recur by checking `run_job.sh` doesn't hardcode the old path:


grep -n "tools\|api_cache" /boot/config/plugins/varaverk/Plugin/unraid/run_job.sh | head -10

Check run_job.sh for any hardcoded old paths