CUSTOM_SCRIPTS_DIR, 'SCRIPTS_DIR' => SCRIPTS_DIR, ]); // The assistant dock renders only where the AI tab itself would: HOST1, with AI_ENABLED true. // The same gate function, not a second copy of the condition, so the page cannot offer a chat // the endpoint will refuse — api/ai.php rejects every action on both counts regardless of what // this page draws. With it false the dock markup is never emitted, and every caller into the // dock is guarded by vvAiDockOn(), which reads the element's absence. $_vv_ai_on = vv_ai_ui_on(); // 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); $_mainManual = in_array('Manual.md', $_repoFiles) ? 'Manual.md' : null; $_childManuals = array_values(array_filter($_repoFiles, function($f) { return preg_match('/^Manual/i', basename($f)) && $f !== 'Manual.md'; })); sort($_childManuals); // Job stats for notification board $totalJobs = 0; $scheduledJobs = 0; foreach ($tree as $orch) { $totalJobs++; if (!empty($orch['cron']) && $orch['enabled']) $scheduledJobs++; foreach (($orch['children'] ?? []) as $child) { $totalJobs++; if (!empty($child['cron']) && $child['enabled']) $scheduledJobs++; } } $runningScripts = []; exec('pgrep -af bash 2>/dev/null', $psLines); foreach ($psLines as $line) { if (preg_match('#' . preg_quote(SCRIPTS_DIR, '#') . '/([^\s]+\.sh)#', $line, $rm)) { $runningScripts[] = basename($rm[1], '.sh'); } } $runningScripts = array_unique($runningScripts); ?>
Not enterprise HA. No SLAs, no quorum nodes, no guaranteed uptime — don't put your billing system on this. What it is: mutual automatic failover between two independent unRAID servers. When one goes down the other starts its containers, cuts over DNS, and keeps users online. When it comes back, everything hands back in the correct sequence — DDNS stops, containers stop, rsync writeback runs, containers start on the primary, primary DDNS starts last — so users hit the returning server only after it's actually ready. Built from scratch. Refined through a year of production testing. The DDNS sequencing and handback order were the hardest parts to get right. Both directions are exercised regularly with fallback_test.sh.
= htmlspecialchars($orch['suggested_cron']) ?>
">
= ($_or || $orch['enabled']) ? '●' : '○' ?>