1	{
2	    "Orchestrators\/array_started.sh": {
3	        "id": "Orchestrators\/array_started.sh",
4	        "enabled": true,
5	        "cron": "array_start",
6	        "log_enabled": false,
7	        "updated": "2026-06-01T00:00:00+00:00"
8	    },
9	    "Orchestrators\/array_stopping.sh": {
10	        "id": "Orchestrators\/array_stopping.sh",
11	        "enabled": true,
12	        "cron": "array_stop",
13	        "log_enabled": false,
14	        "updated": "2026-06-01T00:00:00+00:00"
15	    },
16	    "Orchestrators\/critical_sync_maintenance.sh": {
17	        "id": "Orchestrators\/critical_sync_maintenance.sh",
18	        "enabled": true,
19	        "cron": "*\/30 * * * *",
20	        "log_enabled": false,
21	        "updated": "2026-06-01T00:00:00+00:00"
22	    },
23	    "Orchestrators\/daily_sync_maintenance.sh": {
24	        "id": "Orchestrators\/daily_sync_maintenance.sh",
25	        "enabled": true,
26	        "cron": "0 1 * * *",
27	        "log_enabled": false,
28	        "updated": "2026-06-01T00:00:00+00:00"
29	    },
30	    "Orchestrators\/intermediate_sync_maintenance.sh": {
31	        "id": "Orchestrators\/intermediate_sync_maintenance.sh",
32	        "enabled": true,
33	        "cron": "0 *\/4 * * *",
34	        "log_enabled": false,
35	        "updated": "2026-06-01T00:00:00+00:00"
36	    },
37	    "Orchestrators\/monthly_maintenance.sh": {
38	        "id": "Orchestrators\/monthly_maintenance.sh",
39	        "enabled": true,
40	        "cron": "0 0 15 * *",
41	        "log_enabled": false,
42	        "updated": "2026-06-01T00:00:00+00:00"
43	    },
44	    "Orchestrators\/sunday_morning_coffee_report.sh": {
45	        "id": "Orchestrators\/sunday_morning_coffee_report.sh",
46	        "enabled": true,
47	        "cron": "0 7 * * 0",
48	        "log_enabled": false,
49	        "updated": "2026-06-01T00:00:00+00:00"
50	    },
51	    "Orchestrators\/transcode_management.sh": {
52	        "id": "Orchestrators\/transcode_management.sh",
53	        "enabled": true,
54	        "cron": "*\/7 * * * *",
55	        "log_enabled": false,
56	        "updated": "2026-06-01T20:57:55-04:00"
57	    },
58	    "Orchestrators\/watchdog_orchestrator.sh": {
59	        "id": "Orchestrators\/watchdog_orchestrator.sh",
60	        "enabled": true,
61	        "cron": "*\/15 * * * *",
62	        "log_enabled": false,
63	        "updated": "2026-06-01T20:57:56-04:00"
64	    },
65	    "Orchestrators\/weekly_sync_maintenance.sh": {
66	        "id": "Orchestrators\/weekly_sync_maintenance.sh",
67	        "enabled": true,
68	        "cron": "30 2 * * 0",
69	        "log_enabled": false,
70	        "updated": "2026-06-01T00:00:00+00:00"
71	    },
72	    "Tools\/api_cache_writer.sh": {
73	        "id": "Tools\/api_cache_writer.sh",
74	        "enabled": true,
75	        "cron": "* * * * *",
76	        "log_enabled": false,
77	        "updated": "2026-06-03T00:00:00+00:00"
78	    },
79	    "Tools\/remote_arr_cache_writer.sh": {
80	        "id": "Tools\/remote_arr_cache_writer.sh",
81	        "enabled": true,
82	        "cron": "0 *\/2 * * *",
83	        "log_enabled": false,
84	        "updated": "2026-06-03T00:00:00+00:00"
85	    }
86	}


Nuanced — conf_populate.sh, isnt this specific to unraid. i imagine i will need a different script for other distros..... this should be called in the setup wizard... but that coming up soon


Edit

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

    "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 successfully. (file state is current in your context — no need to Read it back)

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

    "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"
    }

-        "id": "Tools\/api_cache_writer.sh",

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

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

-    "Tools\/remote_arr_cache_writer.sh": {

-        "id": "Tools\/remote_arr_cache_writer.sh",

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

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


Trigger a cron rebuild so the live cron picks up the new paths immediately:


Bash

php -r "
define('VV_PLUGIN_DIR', '/boot/config/plugins/varaverk/Plugin/unraid');
require_once '/boot/config/plugins/varaverk/Plugin/unraid/include/scheduler.php';
\$schedule = vv_schedule_load();
\$ok = vv_cron_rebuild(\$schedule);
echo \$ok ? 'cron rebuilt ok' : 'cron rebuild failed';
" 2>&1

Rebuild cron with updated schedule paths


cron rebuilt ok


Verify cron now points to new paths


* * * * * 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"


Cron is live. Now commit:


git add Plugin/unraid/tools/ Plugin/unraid/api/arrs.php Plugin/unraid/api/storage.php && \
git commit -m "$(cat <<'EOF'
refactor: move unraid-specific tools into Plugin/unraid/tools/

api_cache_writer, remote_arr_cache_writer, conf_populate, storage_migrate are
all Unraid-adapter-specific — they read Docker appdata paths, Unraid service
configs, and drive the Unraid web plugin cache. They don't belong in Tools/.

- Tools/{4 scripts + php} → Plugin/unraid/tools/
- Fix source depth: ../ → ../../../ (now 3 levels from repo root)
- Fix conf_populate SCRIPTS_ROOT bug: was never set, would expand to empty
- api/arrs.php, api/storage.php: SCRIPTS_DIR/Tools/ → dirname(__DIR__)/tools/
- schedule.json: key IDs updated to Plugin/unraid/tools/...
- Cron rebuilt — live entries updated immediately
EOF
)"

Commit the move and all related fixes


[main 17012bc] refactor: move unraid-specific tools into Plugin/unraid/tools/
 7 files changed, 6 insertions(+), 5 deletions(-)
 rename {Tools => Plugin/unraid/tools}/api_cache_writer.php (100%)
 rename {Tools => Plugin/unraid/tools}/api_cache_writer.sh (100%)
 rename {Tools => Plugin/unraid/tools}/conf_populate.sh (99%)
 rename {Tools => Plugin/unraid/tools}/remote_arr_cache_writer.sh (99%)
 rename {Tools => Plugin/unraid/tools}/storage_migrate.sh (99%)


GIT_SSH_COMMAND="ssh -i /boot/config/plugins/varaverk/Configurations/gitea_key -p 221" git push origin main 2>&1

Push to Gitea


Warning: Identity file /boot/config/plugins/varaverk/Configurations/gitea_key not accessible: No such file or directory.
remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://192.168.50.2:221/FailedProxy/Varaverk.git
   bdcdfea..17012bc  main -> main


Done and pushed. Summary:

- All four scripts (+ `api_cache_writer.php`) are now in `Plugin/unraid/tools/` — the Unraid adapter layer where they belong
- `source` depth fixed from `../` to `../../../` in the three bash scripts that load config
- **`conf_populate.sh` bug fixed**: `$SCRIPTS_ROOT` was referenced but never set — now assigned from `$SCRIPTS_DIR` right after sourcing load_config
- PHP references in `arrs.php` and `storage.php` changed from `SCRIPTS_DIR . '/Tools/...'` to `dirname(__DIR__) . '/tools/...'` — no longer needs SCRIPTS_DIR, stays self-contained within the plugin
- `schedule.json` IDs updated, cron rebuilt immediately — live cron already points to the new paths

When you build for TrueNAS or another platform later, the pattern is clear: `Plugin/truenas/tools/` with its own implementations of these four scripts.


ok so scheduler page we should add the plugin/***/tools to the current tools card, merge them. then all basic tools and os specific tools will show in tools card... we should exclude the writers


Let me read the scheduler page first to understand how the Tools card is built.


<system-reminder>[Truncated: PARTIAL view — showing lines 1-923 of 4208 total (67783 tokens, cap 25000). Call Read with offset=924 limit=923 for the next page, or Grep to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]</system-reminder>

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);
51	$_mainManual   = in_array('Manual.md', $_repoFiles) ? 'Manual.md' : null;
52	$_childManuals = array_values(array_filter($_repoFiles, function($f) {
53	    return preg_match('/^Manual/i', basename($f)) && $f !== 'Manual.md';
54	}));
55	sort($_childManuals);
56
57	// Job stats for notification board
58	$totalJobs = 0; $scheduledJobs = 0;
59	foreach ($tree as $orch) {
60	    $totalJobs++;
61	    if (!empty($orch['cron']) && $orch['enabled']) $scheduledJobs++;
62	    foreach (($orch['children'] ?? []) as $child) {
63	        $totalJobs++;
64	        if (!empty($child['cron']) && $child['enabled']) $scheduledJobs++;
65	    }
66	}
67	$runningScripts = [];
68	exec('pgrep -af bash 2>/dev/null', $psLines);
69	foreach ($psLines as $line) {
70	    if (preg_match('#' . preg_quote(SCRIPTS_DIR, '#') . '/([^\s]+\.sh)#', $line, $rm)) {
71	        $runningScripts[] = basename($rm[1], '.sh');
72	    }
73	}
74	$runningScripts = array_unique($runningScripts);
75	?>
76
77

78

79
80
81

82

83
84	      <?php foreach ($tree as $orch): $oid = htmlspecialchars($orch['id']); ?>
85	        <div class="vv-card vv-wide vv-sched-card" data-id="<?= $oid ?>" data-conf-arrays="<?= htmlspecialchars(implode(',', $orch['conf_arrays'] ?? [])) ?>">
86
87

88
89	              <input type="checkbox" class="vv-enabled"
90	                <?= $orch['enabled'] ? 'checked' : '' ?>
91	                onchange="vvSaveOrch(this)">
92
93
94	            ⚙<?= htmlspecialchars($orch['label']) ?>
95	            <?php if (str_starts_with($orch['cron'], 'array_')): ?>
96	              <?= $orch['cron'] === 'array_start' ? '⚡ Array Start' : '⚡ Array Stop' ?>
97	            <?php endif; ?>
98	            <input type="text" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>"
99	              placeholder="cron  or array_start / array_stop"
100	              onblur="vvSaveCronBlur(this)">
101
102

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

105	          <?php endif; ?>
106

107	            <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">▶ Run</button>
108	            <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ Dry Run</button>
109	            <?php $orchLog = vv_job_log_path($orch['id']); ?>
110	            <button class="vv-btn-sm vv-log-btn<?= (file_exists($orchLog) && filesize($orchLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
111
112	              <input type="checkbox" class="vv-log-enabled"
113	                <?= $orch['log_enabled'] ? 'checked' : '' ?>
114	                onchange="vvSaveJob(this)">
115	              Verbose
116
117
118	            <?php if (!empty($orch['children'])): ?>
119	              <button class="vv-advanced-toggle" style="margin-left:auto;width:110px" onclick="vvToggleAdvanced(this)">Advanced ▸</button>
120	            <?php endif; ?>
121

122
123	          <?php if (!empty($orch['children'])): ?>
124	            <div class="vv-children" data-conf-arrays="<?= htmlspecialchars(implode(',', $orch['conf_arrays'] ?? [])) ?>" style="display:none;">
125	              <?php foreach ($orch['children'] as $child): $cid = htmlspecialchars($child['id']); ?>
126	                <?php if (($child['type'] ?? 'script') === 'conf_flag'):
127	                  $_rsync    = vv_rsync_standalone($child['flag_name']);
128	                  $_rsyncFn  = htmlspecialchars($child['flag_name']);
129	                  $_rsyncOid = htmlspecialchars($orch['id']);
130	                ?>
131	                <div class="vv-script" data-id="<?= $cid ?>"
132	                     data-type="conf_flag"
133	                     data-flag-name="<?= htmlspecialchars($child['flag_name']) ?>"
134	                     data-flag-value="<?= !empty($child['flag_value']) ? '1' : '0' ?>">
135

136	                    <label class="vv-toggle" title="Toggle <?= htmlspecialchars($child['flag_name']) ?> in master.conf">
137	                      <input type="checkbox" class="vv-enabled"
138	                        <?= !empty($child['flag_value']) ? 'checked' : '' ?>
139	                        onchange="vvSaveChild(this)">
140
141
142	                    <?= htmlspecialchars($child['label']) ?>
143	                    <span class="vv-flag-badge"<?= empty($child['flag_value']) ? ' style="display:none"' : '' ?>><?= htmlspecialchars($child['flag_name']) ?>
144	                    <input type="text" class="vv-cron vv-rsync-standalone"
145	                           value="<?= htmlspecialchars($_rsync['cron']) ?>"
146	                           placeholder="standalone cron">
147
148

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

151	                  <?php endif; ?>
152

153	                    <button class="vv-btn-sm vv-run-btn vv-rsync-standalone" onclick="vvRunJob(this)">▶ Run</button>
154	                    <button class="vv-btn-sm vv-dry-btn vv-rsync-standalone" onclick="vvDryRun(this)">▶ Dry Run</button>
155	                    <?php $childLog = vv_job_log_path($child['id']); ?>
156	                    <button class="vv-btn-sm vv-log-btn<?= (file_exists($childLog) && filesize($childLog) > 0) ? ' vv-has-log' : '' ?> vv-rsync-standalone" onclick="vvSelectLog(this)">Log</button>
157	                    <input type="text" class="vv-rsync-location vv-rsync-standalone"
158	                           value="<?= htmlspecialchars($_rsync['location']) ?>"
159	                           placeholder="/mnt/user/path"
160	                           data-flag="<?= $_rsyncFn ?>"
161	                           data-orch-id="<?= $_rsyncOid ?>">
162	                    <button class="vv-btn-sm vv-rsync-save-btn vv-rsync-standalone"
163	                            onclick="vvSaveRsyncStandalone(this)">Save</button>
164
165

166

167	                <?php else: ?>
168	                <div class="vv-script" data-id="<?= $cid ?>"
169	                     data-conf-managed="<?= $child['conf_managed'] ? '1' : '0' ?>"
170	                     data-conf-enabled="<?= $child['conf_enabled'] === true ? '1' : ($child['conf_enabled'] === false ? '0' : '') ?>"
171	                     data-conf-array="<?= htmlspecialchars($child['conf_array'] ?? '') ?>">
172

173
174	                      <input type="checkbox" class="vv-enabled"
175	                        <?= $child['enabled'] ? 'checked' : '' ?>
176	                        onchange="vvSaveChild(this)">
177
178
179	                    ⚙<?= htmlspecialchars($child['label']) ?>
180	                    <input type="text" class="vv-cron" value="<?= htmlspecialchars($child['cron']) ?>"
181	                      placeholder="cron  (orch off only)"
182	                      onblur="vvSaveCronBlur(this)">
183
184

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

187	                  <?php endif; ?>
188

189	                    <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">▶ Run</button>
190	                    <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ Dry Run</button>
191	                    <?php $childLog = vv_job_log_path($child['id']); ?>
192	                    <button class="vv-btn-sm vv-log-btn<?= (file_exists($childLog) && filesize($childLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
193
194	                      <input type="checkbox" class="vv-log-enabled"
195	                        <?= $child['log_enabled'] ? 'checked' : '' ?>
196	                        onchange="vvSaveJob(this)">
197	                      Verbose
198
199	                    <input type="text" class="vv-script-args" placeholder="extra args"
200	                      style="display:<?= $orch['enabled'] ? 'none' : '' ?>">
201
202

203

204	                <?php endif; ?>
205	              <?php endforeach; ?>
206

207	          <?php endif; ?>
208
209

210	      <?php endforeach; ?>
211
212
213

214

215	          Tools
216	          <?= count($tools) ?> tool<?= count($tools) !== 1 ? 's' : '' ?>
217	          <button class="vv-advanced-toggle" style="margin-left:auto;width:100px" onclick="vvToggleTools(this)">Tools ▸</button>
218

219

220	          <?php if (empty($tools)): ?>
221
No scripts found in Tools/.

222	          <?php else: ?>
223	            <?php foreach ($tools as $ts): $tsid = htmlspecialchars($ts['id']); ?>
224	              <div class="vv-script" data-id="<?= $tsid ?>">
225

226
227	                    <input type="checkbox" class="vv-enabled"
228	                      <?= $ts['enabled'] ? 'checked' : '' ?>
229	                      onchange="vvSaveJob(this)">
230
231
232	                  <?= htmlspecialchars($ts['label']) ?>
233	                  <input type="text" class="vv-cron" value="<?= htmlspecialchars($ts['cron']) ?>"
234	                    placeholder="cron expression"
235	                    onblur="vvSaveCronBlur(this)">
236
237

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

240	                <?php endif; ?>
241

242	                  <button class="vv-btn-sm vv-run-btn" onclick="vvRunJob(this)">▶ Run</button>
243	                  <button class="vv-btn-sm vv-dry-btn" onclick="vvDryRun(this)">▶ Dry Run</button>
244	                  <?php $tsLog = vv_job_log_path($ts['id']); ?>
245	                  <button class="vv-btn-sm vv-log-btn<?= (file_exists($tsLog) && filesize($tsLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>
246
247	                    <input type="checkbox" class="vv-log-enabled"
248	                      <?= $ts['log_enabled'] ? 'checked' : '' ?>
249	                      onchange="vvSaveJob(this)">
250	                    Verbose
251
252	                  <input type="text" class="vv-script-args" placeholder="extra args">
253
254

255

256	            <?php endforeach; ?>
257	          <?php endif; ?>
258

259

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

380	            <button id="vv-back-btn" class="vv-btn-sm" onclick="vvBackToSuggestions()" style="display:none">☰ Scheduler Info</button>
381	            <button id="vv-restore-btn" class="vv-btn-sm" onclick="vvRestoreLastLog()" style="display:none">← </button>
382	            Scheduler Information
383
384

385

386
387	              <input type="checkbox" id="vv-auto-scroll" checked> Auto Scroll
388
389
390	              <input type="checkbox" id="vv-invert-log" onchange="vvToggleInvert(this)"> Invert
391
392	            <button id="vv-cancel-edit-btn" class="vv-btn-sm vv-delete-btn" onclick="vvBackToSuggestions()" style="display:none">Cancel</button>
393	            <button id="vv-undo-btn" class="vv-btn-sm vv-undo-redo-btn" onclick="vvUndo()" style="display:none" disabled title="Undo (Ctrl+Z)">↩ Undo</button>
394	            <button id="vv-redo-btn" class="vv-btn-sm vv-undo-redo-btn" onclick="vvRedo()" style="display:none" disabled title="Redo (Ctrl+Y / Ctrl+Shift+Z)">↪ Redo</button>
395	            <button id="vv-save-script-btn" class="vv-btn-sm vv-save-script-btn-style" onclick="vvSaveScript()" style="display:none">Save Script</button>
396	            <button id="vv-save-conf-btn" class="vv-btn-sm vv-save-script-btn-style" onclick="vvSaveConf()" style="display:none">Save Config</button>
397	            <button id="vv-save-rawconf-btn" class="vv-btn-sm vv-save-script-btn-style" onclick="vvSaveRawConf()" style="display:none">Save Conf</button>
398
399	            <input type="text" id="vv-log-search" placeholder="Search…"
400	                   oninput="vvFilterLog(this.value)" style="display:none;">
401	            <button id="vv-stop-btn" class="vv-btn-sm" onclick="vvStopJob()" style="display:none" title="Stop running script and clear any stuck locks">■ Stop</button>
402	            <button id="vv-clear-btn" class="vv-btn-sm" onclick="vvClearRightLog()" style="display:none">Clear</button>
403	            <button id="vv-advanced-mode-btn" class="vv-btn-sm vv-adv-mode-btn" onclick="vvToggleAdvancedMode()" title="Toggle Advanced mode — shows full script source and raw conf editing">Advanced</button>
404

405

406
407
408

409
410
411

412	            <?= $scheduledJobs ?> / <?= $totalJobs ?> scheduled
413	            ·
414	            <span class="vv-nb-stat <?= !empty($runningScripts) ? 'vv-nb-running' : '' ?>">
415	              <?= !empty($runningScripts) ? 'Running: ' . htmlspecialchars(implode(', ', $runningScripts)) : 'Idle' ?>
416
417	            ·
418	            <?php $confFiles = vv_get_conf_files(); ?>
419	            <?php if (!empty($confFiles)): ?>
420
421	                <?php foreach ($confFiles as $cf): ?>
422	                  <button class="vv-btn-sm vv-nb-conf-btn" onclick="vvEditRawConf('<?= htmlspecialchars($cf) ?>')"><?= htmlspecialchars($cf) ?></button>
423	                <?php endforeach; ?>
424
425	            <?php endif; ?>
426	            <button class="vv-btn-sm vv-adv-only" id="vv-git-pull-btn"
427	                    onclick="vvGitPull(this)"
428	                    title="Pull latest scripts from repository and run conf upgrade"
429	                    style="display:none;margin-left:auto;">↻ Git Pull</button>
430

431
432
433

434

435	              ▾
436	              How do I use this
437

438

439

440
Toggle — saves immediately to schedule.json

441
Cron field — auto-saved on focus-out; hint shows plain English; red border = invalid

442
⚙ Cog — opens settings panel: script header + matching README/Manual sections + config fields

443
Script name — click navigates to the matching entry in the Orch tree (right panel)

444
Run — fires script immediately; resets the cron timer so next fire is one full interval later

445
Dry Run — same as Run but passes --dry-run; no changes written to disk

446
Log — opens script log here; auto-scrolls; use Search box to filter/highlight lines

447
Stop — SIGTERM → 3 s → SIGKILL; clears stuck lock files

448
Verbose — appends --log flag for per-item detail output

449
Cancel — discards changes and returns here; file is never touched until Save

450
⚡ Array events — set any cron field to array_start or array_stop; badge appears; fires on Unraid array start/stop; works on any orch or custom script

451
Arrange — drag scripts between orchs; drop to right panel removes from orch; Save Arrangement commits to master.conf

452
+ Folder — collapsible subfolder in Custom Scripts; drag scripts in

453
Suggested cron — click the cron code badge in the Orch tree to apply and save instantly

454
Status dot (tree) — green/orange/red ● = last run ok/warn/error; dim = never run; hover for detail

455
Cron Calculator — type an expression or plain English; shows description + next 5 fires; Apply pushes to last-focused cron field

456
Recent Activity — last 24 runs; click a row to open that log; auto-expands on errors

457
Recent Errors — last error per script (7 days); click to open log; auto-expands on new errors

458
Advanced — top-right button, turns blue; ⚙ shows enriched view (header + docs + config); raw conf editing unlocked

459
Log search — filter box in toolbar when log is open; highlights matches, dims others

460
← Scheduler Info — returns here from any log, editor, conf, or script view

461
Orchestrator & Child Behaviour

462
Orch ON — sole trigger; cron fires the orch; it calls children in sequence

463
Orch OFF — never runs automatically; children suppressed; each can get its own standalone cron

464
Child (orch ON) — toggle comments/uncomments the script in master.conf

465
Child (orch ON, no array) — orch hardcodes the call; toggle is display-only

466
Child (orch OFF) — enter a cron to run it standalone

467
Rsync badge — toggle writes TIER_RSYNC_ENABLED directly to master.conf

468
Rsync (orch OFF) — fill location + standalone cron + Save; both required for independent firing

469

470

471

472
473
474

475

476	              ▾
477	              Next Runs
478

479

480
calculating…

481

482

483
484
485

486

487	              ▸
488	              Cron Calculator
489

490

491

492	                <input type="text" id="vv-calc-input" class="vv-calc-in"
493	                       placeholder="*/15 * * * *  or  every day at 3am"
494	                       oninput="vvCalcUpdate(this.value)"
495	                       onkeydown="if(event.key==='Enter')vvCalcApply()">
496

497	                <button class="vv-btn-sm vv-calc-apply-btn" id="vv-calc-apply"
498	                        onclick="vvCalcApply()" style="display:none">
499	                  Apply to selected
500	                </button>
501

502

503

504
505
506

507

508	              ▸
509	              Recent Activity
510
511

512

513
loading…

514

515

516
517
518

519

520	              ▸
521	              Recent Errors
522
523

524

525
loading…

526

527

528
529
530

531

532	              ▸
533	              Active Locks
534
535

536

537
loading…

538

539

540
541
542

543

544	              ▸
545	              Partner
546	              —
547

548

549
loading…

550

551

552
553
554	          <?php
555	          $_confMap  = vv_conf_script_map();
556	          $_disabled = array_filter($_confMap, fn($v) => $v['enabled'] === false);
557	          ?>
558

559

560	              ▸
561	              Disabled on this host
562	              <?php if (!empty($_disabled)): ?>
563	                <?= count($_disabled) ?>
564	              <?php endif; ?>
565

566

567	              <?php if (empty($_disabled)): ?>
568
All scripts enabled.

569	              <?php else: ?>
570

571	                <?php foreach ($_disabled as $_drel => $_dv):
572	                  $_dname = str_replace('_', ' ', strtolower(preg_replace('/_SCRIPTS$/', '', $_dv['array'] ?? '')));
573	                ?>
574

575	                    <?= htmlspecialchars(basename($_drel, '.sh')) ?>
576	                    <?= htmlspecialchars($_dname) ?>
577

578	                <?php endforeach; ?>
579

580	              <?php endif; ?>
581

582

583
584
585

586

587	              ▾
588	              Fallback
589

590

591
Not enterprise HA. No SLAs, no quorum nodes, no guaranteed uptime — don't put your billing system on this.
592
593	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.
594
595	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.

596

597

598
599
600

601

602	              ▸
603	              How it works
604

605

606
Varaverk owns the cron layer. schedule.json stores every orchestrator's enabled state, cron expression, and log setting. When you save, the plugin rebuilds /etc/cron.d/varaverk — one entry per enabled orchestrator. That's all cron knows about.
607
608	Orchestrators are the only scripts with cron schedules. Each one reads its SCRIPTS list out of master.conf and runs each child in sequence. The children do the actual work. The orchestrator controls the order and the timing. A child failure stops the chain.
609
610	master.conf is the per-host control plane. Comment out a line to disable that script on this host without touching any code. Both servers run the same repository — different master.conf comments produce different behaviour per machine.
611
612	Children only get their own cron when their orchestrator is OFF. When the orch is on, it owns the timing completely.
613
614	common.sh provides the shared runtime every script sources: acquire_lock(), log_msg(), info(), success(), warn(), error(). Lock files live in /tmp/varaverk_locks/. acquire_lock() exits cleanly if a prior run is still in progress — nothing piles up.
615
616	Logs are written per script to the appdata path. The Log button shows the latest run. Verbose mode passes --log for per-step detail.
617
618	Array events bypass cron entirely. array_start fires when Unraid brings the array up; array_stop fires before shutdown. array_started handles all post-start setup in sequence; array_stopping handles the graceful teardown.
619
620	The watchdog tier runs every 15 minutes as a single-pass chain: resource_watchdog frees memory pressure first, docker_watchdog restarts down containers into that headroom, system_watchdog checks storage/WebGUI/network, stability_watchdog reboots as a last resort. The order matters — restarting containers before freeing memory just restarts them into the same pressure.

621

622

623
624
625

626

627	              ▸
628	              This started as...
629

630

631
One script. A few lines — checking whether a container was running and restarting it if not. Backgrounded through User Scripts, sleeping a minute between checks, essentially forgotten. It did its job.
632
633	Then a conversation with a buddy: wouldn't it be cool if it emailed when something crashed? Sure, added it. Wouldn't it be cool if it handled more containers? Easy — grow the list. Wouldn't it be cool if both servers stayed in sync automatically so neither of us had to remember to do it manually?
634
635	That one took a while.
636
637	Order started to matter. Freeing memory before attempting container restarts meant the containers actually stayed up. Sync had to happen before the media database refresh, not after. Watchdogs couldn't block each other. The big loop that tried to do everything started breaking into pieces — one piece per job, and a sequencer that knew which piece went first.
638
639	master.conf became the control plane: one file, two different sets of comments for two different hosts. Same codebase, different behaviour per machine. The orchestrator pattern came out of not wanting to duplicate logic.
640
641	By the time the User Scripts plugin couldn't clearly show what was actually scheduled, whether something was running, or why a sync had quietly stopped working two weeks ago — the interface became the next thing to build.
642
643	Varaverk started as a status table and a schedule editor. The scheduler became the main thing. The monitor followed.
644
645	Still the same two servers, two households, the same media stack running itself. The scripts just grew up around it.

646

647

648
649
650
Docs

651

652
653	          <?php if ($_mainReadme || !empty($_childReadmes)): ?>
654

655

660	              <?php if (!empty($_childReadmes)): ?>
661	                ▸
662	              <?php else: ?>
663
664	              <?php endif; ?>
665	              README
666

667	            <?php if (!empty($_childReadmes)): ?>
668

669	              <?php foreach ($_childReadmes as $_cr):
670	                $_crLabel = preg_replace('/^README-?/i', '', basename($_cr));
671	                $_crLabel = preg_replace('/\.md$/i', '', $_crLabel);
672	                $_crLabel = str_replace('_', ' ', $_crLabel) ?: dirname($_cr);
673	              ?>
674	              <div class="vv-sb-row vv-sb-child-row"
675	                   data-id="<?= htmlspecialchars($_cr) ?>"
676	                   data-hdr=""
677	                   data-desc=""
678	                   onclick="vvSelectScript(this)">
679	                └
680	                <?= htmlspecialchars($_crLabel) ?>
681

682	              <?php endforeach; ?>
683

684	            <?php endif; ?>
685

686	          <?php endif; ?>
687
688	          <?php if ($_mainManual || !empty($_childManuals)): ?>
689

690

695	              <?php if (!empty($_childManuals)): ?>
696	                ▸
697	              <?php else: ?>
698
699	              <?php endif; ?>
700	              Manual
701

702	            <?php if (!empty($_childManuals)): ?>
703

704	              <?php foreach ($_childManuals as $_cm):
705	                $_cmLabel = preg_replace('/^Manual-?/i', '', basename($_cm));
706	                $_cmLabel = preg_replace('/\.md$/i', '', $_cmLabel);
707	                $_cmLabel = str_replace('_', ' ', $_cmLabel) ?: dirname($_cm);
708	              ?>
709	              <div class="vv-sb-row vv-sb-child-row"
710	                   data-id="<?= htmlspecialchars($_cm) ?>"
711	                   data-hdr=""
712	                   data-desc=""
713	                   onclick="vvSelectScript(this)">
714	                └
715	                <?= htmlspecialchars($_cmLabel) ?>
716

717	              <?php endforeach; ?>
718

719	            <?php endif; ?>
720

721	          <?php endif; ?>
722
723

724
725
726
Orch

727

728	          <?php foreach ($tree as $orch):
729	            $orchId   = $orch['id'];
730	            $orchHdr  = vv_script_header_clean(SCRIPTS_DIR . '/' . $orchId);
731	            $hasKids  = !empty($orch['children']);
732	          ?>
733

734	            <div class="vv-sb-row vv-sb-orch-row"
735	                 data-id="<?= htmlspecialchars($orchId) ?>"
736	                 data-hdr="<?= htmlspecialchars($orchHdr) ?>"
737	                 data-desc="<?= htmlspecialchars($orch['desc']) ?>"
738	                 onclick="vvSelectScript(this)">
739	              <?php if ($hasKids): ?>
740	                ▸
741	              <?php else: ?>
742
743	              <?php endif; ?>
744	              <?= htmlspecialchars($orch['label']) ?>
745	              <?php if (str_starts_with($orch['cron'], 'array_')): ?>
746	                ⚡
747	              <?php elseif ($orch['suggested_cron']): ?>
748	                <?= htmlspecialchars($orch['suggested_cron']) ?>
749	              <?php endif; ?>
750	              <?php
751	                $_os = @json_decode(@file_get_contents(vv_job_stat_path($orchId)), true);
752	                $_or = $_os['status'] ?? '';
753	                $_oc = $_or === 'ok' ? 'vv-stat-ok' : ($_or === 'warn' ? 'vv-stat-warn' : ($_or === 'error' ? 'vv-stat-error' : ($_or === 'skipped' ? 'vv-stat-skip' : ($orch['enabled'] ? 'vv-sug-on' : 'vv-stat-none'))));
754	              ?>
755	              <span class="<?= $_oc ?> vv-sb-status" title="<?= $_or ? htmlspecialchars("last: $_or") : ($orch['enabled'] ? 'enabled' : 'never run') ?>">
756	                <?= ($_or || $orch['enabled']) ? '●' : '○' ?>
757
758

759	            <?php if ($hasKids): ?>
760

761	              <?php foreach ($orch['children'] as $child):
762	                $childHdr = vv_script_header_clean(SCRIPTS_DIR . '/' . $child['id']);
763	              ?>
764	              <div class="vv-sb-row vv-sb-child-row"
765	                   data-id="<?= htmlspecialchars($child['id']) ?>"
766	                   data-hdr="<?= htmlspecialchars($childHdr) ?>"
767	                   data-desc="<?= htmlspecialchars($child['desc']) ?>"
768	                   onclick="vvSelectScript(this)">
769	                └
770	                <?= htmlspecialchars($child['label']) ?>
771	                <?php if (!empty($child['suggested_cron'])): ?>
772	                  <?= htmlspecialchars($child['suggested_cron']) ?>
773	                <?php endif; ?>
774	                <?php
775	                  $_cs = @json_decode(@file_get_contents(vv_job_stat_path($child['id'])), true);
776	                  $_cr = $_cs['status'] ?? '';
777	                  $_cc = $_cr === 'ok' ? 'vv-stat-ok' : ($_cr === 'warn' ? 'vv-stat-warn' : ($_cr === 'error' ? 'vv-stat-error' : ($_cr === 'skipped' ? 'vv-stat-skip' : (!empty($child['conf_enabled']) ? 'vv-sug-on' : 'vv-stat-none'))));
778	                ?>
779	                <span class="<?= $_cc ?> vv-sb-status" title="<?= $_cr ? htmlspecialchars("last: $_cr") : (!empty($child['conf_enabled']) ? 'enabled' : 'never run') ?>">
780	                  <?= ($_cr || !empty($child['conf_enabled'])) ? '●' : '○' ?>
781
782

783	              <?php endforeach; ?>
784

785	            <?php endif; ?>
786

787	          <?php endforeach; ?>
788

789
790
791
Repository

792	          <?php if (!empty($_repoFiles)): ?>
793

794

795	              ▸
796	              Varaverk
797

798

799	              <?php
800	              $_rootFiles = array_values(array_filter($_repoFiles, function($f) { return strpos($f, '/') === false; }));
801	              $_subFiles  = array_values(array_filter($_repoFiles, function($f) { return strpos($f, '/') !== false; }));
802	              // Root-level files under Varaverk/ divider
803	              if (!empty($_rootFiles)):
804	              ?>
805
Varaverk/

806	              <?php foreach ($_rootFiles as $_rel):
807	                $_isScript = (bool)preg_match('/\.sh$/i', $_rel);
808	                $_hdr      = $_isScript ? vv_script_header_clean(SCRIPTS_DIR . '/' . $_rel) : '';
809	              ?>
810	              <div class="vv-sb-row"
811	                   data-id="<?= htmlspecialchars($_rel) ?>"
812	                   data-hdr="<?= htmlspecialchars($_hdr) ?>"
813	                   data-desc=""
814	                   onclick="vvSelectScript(this)"
815	                   style="padding-left:18px">
816
817	                <span class="vv-sb-name" style="color:<?= $_isScript ? '#b0c4d0' : '#7a9eb5' ?>;font-size:12px;">
818	                  <?= htmlspecialchars($_rel) ?>
819
820

821	              <?php endforeach; endif; ?>
822	              <?php
823	              // Subdirectory files
824	              $_seenDirs = [];
825	              foreach ($_subFiles as $_rel):
826	                $_parts    = explode('/', $_rel);
827	                $_dirParts = array_slice($_parts, 0, -1);
828	                $_filename = end($_parts);
829	                $_isScript = (bool)preg_match('/\.sh$/i', $_filename);
830	                for ($_di = 0; $_di < count($_dirParts); $_di++):
831	                  $_dirPath = implode('/', array_slice($_dirParts, 0, $_di + 1));
832	                  if (!in_array($_dirPath, $_seenDirs)):
833	                    $_seenDirs[] = $_dirPath;
834	              ?>
835	              <div class="vv-info-divider" style="padding-left:<?= $_di * 14 + 8 ?>px;font-size:10px;">
836	                <?= htmlspecialchars($_dirParts[$_di]) ?>/
837

838	              <?php
839	                  endif;
840	                endfor;
841	                $_depth = count($_dirParts);
842	                $_hdr   = $_isScript ? vv_script_header_clean(SCRIPTS_DIR . '/' . $_rel) : '';
843	              ?>
844	              <div class="vv-sb-row"
845	                   data-id="<?= htmlspecialchars($_rel) ?>"
846	                   data-hdr="<?= htmlspecialchars($_hdr) ?>"
847	                   data-desc=""
848	                   onclick="vvSelectScript(this)"
849	                   style="padding-left:<?= $_depth * 14 + 4 ?>px">
850
851	                <span class="vv-sb-name" style="color:<?= $_isScript ? '#b0c4d0' : '#7a9eb5' ?>;font-size:12px;">
852	                  <?= htmlspecialchars($_filename) ?>
853
854

855	              <?php endforeach; ?>
856

857

858	          <?php endif; ?>
859
860

861
862
863

864
865
866

867
868
869

870
871
872

873

874	            Name:
875	            <input type="text" id="vv-editor-name" class="vv-cron" style="flex:1" placeholder="script_name  (no .sh)">
876

877

878

879	              ›
880	              <input type="text" id="vv-find-input" placeholder="Find…"
881	                     oninput="vvFindSearch()" onkeydown="vvFindKeydown(event)">
882
883	              <button class="vv-btn-sm vv-find-nav-btn" onclick="vvFindNav(-1)" title="Previous (Shift+Enter)">↑</button>
884	              <button class="vv-btn-sm vv-find-nav-btn" onclick="vvFindNav(1)"  title="Next (Enter)">↓</button>
885	              ×
886

887

888
889	              <input type="text" id="vv-replace-input" placeholder="Replace with…"
890	                     onkeydown="vvReplaceKeydown(event)">
891	              <button class="vv-btn-sm vv-repl-btn" onclick="vvReplaceOne()" title="Replace (Enter)">Replace</button>
892	              <button class="vv-btn-sm vv-repl-btn" onclick="vvReplaceAll()" title="Replace All">All</button>
893

894

895

896	            Go to line
897	            <input type="number" id="vv-goto-input" min="1" placeholder="#"
898	                   oninput="vvGotoApply()" onkeydown="vvGotoKeydown(event)">
899
900	            ×
901

902
903

904

905	              ▾
906	              Keyboard Shortcuts
907

908

909

910
Ctrl+S — save

911
Ctrl+Z / Ctrl+Y — undo / redo (200 steps)

912
Ctrl+/ — toggle line comment

913
Tab / Shift+Tab — indent / dedent

914
Ctrl+F — find

915
Ctrl+H — find & replace

916
Ctrl+G — go to line

917
Ctrl+D — select next occurrence

918
Ctrl+L — select line

919
Ctrl+Shift+K — delete line

920
Alt+↑/↓ — move line

921
Alt+Shift+↓ — duplicate line below

922
Home — smart (first non-space → col 0)

923
( { [ " ` — auto-close pair; wraps selection; Backspace deletes both

<?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);
$_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);
?>

      <?php foreach ($tree as $orch): $oid = htmlspecialchars($orch['id']); ?>
        <div class="vv-card vv-wide vv-sched-card" data-id="<?= $oid ?>" data-conf-arrays="<?= htmlspecialchars(implode(',', $orch['conf_arrays'] ?? [])) ?>">

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

            ⚙<?= htmlspecialchars($orch['label']) ?>
            <?php if (str_starts_with($orch['cron'], 'array_')): ?>
              <?= $orch['cron'] === 'array_start' ? '⚡ Array Start' : '⚡ Array Stop' ?>
            <?php endif; ?>
            <input type="text" class="vv-cron" value="<?= htmlspecialchars($orch['cron']) ?>"
              placeholder="cron  or array_start / array_stop"
              onblur="vvSaveCronBlur(this)">

          <?php if (!empty($orch['desc'])): ?>
            <div class="vv-job-desc" title="<?= htmlspecialchars($orch['desc']) ?>"><?= htmlspecialchars($orch['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>
            <?php $orchLog = vv_job_log_path($orch['id']); ?>
            <button class="vv-btn-sm vv-log-btn<?= (file_exists($orchLog) && filesize($orchLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>

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

            <?php if (!empty($orch['children'])): ?>
              <button class="vv-advanced-toggle" style="margin-left:auto;width:110px" onclick="vvToggleAdvanced(this)">Advanced ▸</button>
            <?php endif; ?>

          <?php if (!empty($orch['children'])): ?>
            <div class="vv-children" data-conf-arrays="<?= htmlspecialchars(implode(',', $orch['conf_arrays'] ?? [])) ?>" style="display:none;">
              <?php foreach ($orch['children'] as $child): $cid = htmlspecialchars($child['id']); ?>
                <?php if (($child['type'] ?? 'script') === 'conf_flag'):
                  $_rsync    = vv_rsync_standalone($child['flag_name']);
                  $_rsyncFn  = htmlspecialchars($child['flag_name']);
                  $_rsyncOid = htmlspecialchars($orch['id']);
                ?>
                <div class="vv-script" data-id="<?= $cid ?>"
                     data-type="conf_flag"
                     data-flag-name="<?= htmlspecialchars($child['flag_name']) ?>"
                     data-flag-value="<?= !empty($child['flag_value']) ? '1' : '0' ?>">

                    <label class="vv-toggle" title="Toggle <?= htmlspecialchars($child['flag_name']) ?> in master.conf">
                      <input type="checkbox" class="vv-enabled"
                        <?= !empty($child['flag_value']) ? 'checked' : '' ?>
                        onchange="vvSaveChild(this)">

                    <?= htmlspecialchars($child['label']) ?>
                    <span class="vv-flag-badge"<?= empty($child['flag_value']) ? ' style="display:none"' : '' ?>><?= htmlspecialchars($child['flag_name']) ?>
                    <input type="text" class="vv-cron vv-rsync-standalone"
                           value="<?= htmlspecialchars($_rsync['cron']) ?>"
                           placeholder="standalone cron">

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

                  <?php endif; ?>

                    <button class="vv-btn-sm vv-run-btn vv-rsync-standalone" onclick="vvRunJob(this)">▶ Run</button>
                    <button class="vv-btn-sm vv-dry-btn vv-rsync-standalone" onclick="vvDryRun(this)">▶ Dry Run</button>
                    <?php $childLog = vv_job_log_path($child['id']); ?>
                    <button class="vv-btn-sm vv-log-btn<?= (file_exists($childLog) && filesize($childLog) > 0) ? ' vv-has-log' : '' ?> vv-rsync-standalone" onclick="vvSelectLog(this)">Log</button>
                    <input type="text" class="vv-rsync-location vv-rsync-standalone"
                           value="<?= htmlspecialchars($_rsync['location']) ?>"
                           placeholder="/mnt/user/path"
                           data-flag="<?= $_rsyncFn ?>"
                           data-orch-id="<?= $_rsyncOid ?>">
                    <button class="vv-btn-sm vv-rsync-save-btn vv-rsync-standalone"
                            onclick="vvSaveRsyncStandalone(this)">Save</button>

                <?php else: ?>
                <div class="vv-script" data-id="<?= $cid ?>"
                     data-conf-managed="<?= $child['conf_managed'] ? '1' : '0' ?>"
                     data-conf-enabled="<?= $child['conf_enabled'] === true ? '1' : ($child['conf_enabled'] === false ? '0' : '') ?>"
                     data-conf-array="<?= htmlspecialchars($child['conf_array'] ?? '') ?>">

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

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

                  <?php if (!empty($child['desc'])): ?>
                    <div class="vv-job-desc" title="<?= htmlspecialchars($child['desc']) ?>"><?= htmlspecialchars($child['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>
                    <?php $childLog = vv_job_log_path($child['id']); ?>
                    <button class="vv-btn-sm vv-log-btn<?= (file_exists($childLog) && filesize($childLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>

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

                    <input type="text" class="vv-script-args" placeholder="extra args"
                      style="display:<?= $orch['enabled'] ? 'none' : '' ?>">

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

          <?php endif; ?>

      <?php endforeach; ?>

          Tools
          <?= count($tools) ?> tool<?= count($tools) !== 1 ? 's' : '' ?>
          <button class="vv-advanced-toggle" style="margin-left:auto;width:100px" onclick="vvToggleTools(this)">Tools ▸</button>

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

No scripts found in Tools/.

          <?php else: ?>
            <?php foreach ($tools as $ts): $tsid = htmlspecialchars($ts['id']); ?>
              <div class="vv-script" data-id="<?= $tsid ?>">

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

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

                <?php if (!empty($ts['desc'])): ?>
                  <div class="vv-job-desc" title="<?= htmlspecialchars($ts['desc']) ?>"><?= htmlspecialchars($ts['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>
                  <?php $tsLog = vv_job_log_path($ts['id']); ?>
                  <button class="vv-btn-sm vv-log-btn<?= (file_exists($tsLog) && filesize($tsLog) > 0) ? ' vv-has-log' : '' ?>" onclick="vvSelectLog(this)">Log</button>

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

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

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

      <?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>

            <button id="vv-back-btn" class="vv-btn-sm" onclick="vvBackToSuggestions()" style="display:none">☰ Scheduler Info</button>
            <button id="vv-restore-btn" class="vv-btn-sm" onclick="vvRestoreLastLog()" style="display:none">← </button>
            Scheduler Information

              <input type="checkbox" id="vv-auto-scroll" checked> Auto Scroll

              <input type="checkbox" id="vv-invert-log" onchange="vvToggleInvert(this)"> Invert

            <button id="vv-cancel-edit-btn" class="vv-btn-sm vv-delete-btn" onclick="vvBackToSuggestions()" style="display:none">Cancel</button>
            <button id="vv-undo-btn" class="vv-btn-sm vv-undo-redo-btn" onclick="vvUndo()" style="display:none" disabled title="Undo (Ctrl+Z)">↩ Undo</button>
            <button id="vv-redo-btn" class="vv-btn-sm vv-undo-redo-btn" onclick="vvRedo()" style="display:none" disabled title="Redo (Ctrl+Y / Ctrl+Shift+Z)">↪ Redo</button>
            <button id="vv-save-script-btn" class="vv-btn-sm vv-save-script-btn-style" onclick="vvSaveScript()" style="display:none">Save Script</button>
            <button id="vv-save-conf-btn" class="vv-btn-sm vv-save-script-btn-style" onclick="vvSaveConf()" style="display:none">Save Config</button>
            <button id="vv-save-rawconf-btn" class="vv-btn-sm vv-save-script-btn-style" onclick="vvSaveRawConf()" style="display:none">Save Conf</button>

            <input type="text" id="vv-log-search" placeholder="Search…"
                   oninput="vvFilterLog(this.value)" style="display:none;">
            <button id="vv-stop-btn" class="vv-btn-sm" onclick="vvStopJob()" style="display:none" title="Stop running script and clear any stuck locks">■ Stop</button>
            <button id="vv-clear-btn" class="vv-btn-sm" onclick="vvClearRightLog()" style="display:none">Clear</button>
            <button id="vv-advanced-mode-btn" class="vv-btn-sm vv-adv-mode-btn" onclick="vvToggleAdvancedMode()" title="Toggle Advanced mode — shows full script source and raw conf editing">Advanced</button>

            <?= $scheduledJobs ?> / <?= $totalJobs ?> scheduled
            ·
            <span class="vv-nb-stat <?= !empty($runningScripts) ? 'vv-nb-running' : '' ?>">
              <?= !empty($runningScripts) ? 'Running: ' . htmlspecialchars(implode(', ', $runningScripts)) : 'Idle' ?>

            ·
            <?php $confFiles = vv_get_conf_files(); ?>
            <?php if (!empty($confFiles)): ?>

                <?php foreach ($confFiles as $cf): ?>
                  <button class="vv-btn-sm vv-nb-conf-btn" onclick="vvEditRawConf('<?= htmlspecialchars($cf) ?>')"><?= htmlspecialchars($cf) ?></button>
                <?php endforeach; ?>

            <?php endif; ?>
            <button class="vv-btn-sm vv-adv-only" id="vv-git-pull-btn"
                    onclick="vvGitPull(this)"
                    title="Pull latest scripts from repository and run conf upgrade"
                    style="display:none;margin-left:auto;">↻ Git Pull</button>

              ▾
              How do I use this

Toggle — saves immediately to schedule.json

Cron field — auto-saved on focus-out; hint shows plain English; red border = invalid

⚙ Cog — opens settings panel: script header + matching README/Manual sections + config fields

Script name — click navigates to the matching entry in the Orch tree (right panel)

Run — fires script immediately; resets the cron timer so next fire is one full interval later

Dry Run — same as Run but passes --dry-run; no changes written to disk

Log — opens script log here; auto-scrolls; use Search box to filter/highlight lines

Stop — SIGTERM → 3 s → SIGKILL; clears stuck lock files

Verbose — appends --log flag for per-item detail output

Cancel — discards changes and returns here; file is never touched until Save

⚡ Array events — set any cron field to array_start or array_stop; badge appears; fires on Unraid array start/stop; works on any orch or custom script

Arrange — drag scripts between orchs; drop to right panel removes from orch; Save Arrangement commits to master.conf

+ Folder — collapsible subfolder in Custom Scripts; drag scripts in

Suggested cron — click the cron code badge in the Orch tree to apply and save instantly

Status dot (tree) — green/orange/red ● = last run ok/warn/error; dim = never run; hover for detail

Cron Calculator — type an expression or plain English; shows description + next 5 fires; Apply pushes to last-focused cron field

Recent Activity — last 24 runs; click a row to open that log; auto-expands on errors

Recent Errors — last error per script (7 days); click to open log; auto-expands on new errors

Advanced — top-right button, turns blue; ⚙ shows enriched view (header + docs + config); raw conf editing unlocked

Log search — filter box in toolbar when log is open; highlights matches, dims others

← Scheduler Info — returns here from any log, editor, conf, or script view

Orchestrator & Child Behaviour

Orch ON — sole trigger; cron fires the orch; it calls children in sequence

Orch OFF — never runs automatically; children suppressed; each can get its own standalone cron

Child (orch ON) — toggle comments/uncomments the script in master.conf

Child (orch ON, no array) — orch hardcodes the call; toggle is display-only

Child (orch OFF) — enter a cron to run it standalone

Rsync badge — toggle writes TIER_RSYNC_ENABLED directly to master.conf

Rsync (orch OFF) — fill location + standalone cron + Save; both required for independent firing

              ▾
              Next Runs

calculating…

              ▸
              Cron Calculator

                <input type="text" id="vv-calc-input" class="vv-calc-in"
                       placeholder="*/15 * * * *  or  every day at 3am"
                       oninput="vvCalcUpdate(this.value)"
                       onkeydown="if(event.key==='Enter')vvCalcApply()">

                <button class="vv-btn-sm vv-calc-apply-btn" id="vv-calc-apply"
                        onclick="vvCalcApply()" style="display:none">
                  Apply to selected
                </button>

              ▸
              Recent Activity

loading…

              ▸
              Recent Errors

loading…

              ▸
              Active Locks

loading…

              ▸
              Partner
              —

loading…

          <?php
          $_confMap  = vv_conf_script_map();
          $_disabled = array_filter($_confMap, fn($v) => $v['enabled'] === false);
          ?>

              ▸
              Disabled on this host
              <?php if (!empty($_disabled)): ?>
                <?= count($_disabled) ?>
              <?php endif; ?>

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

All scripts enabled.

              <?php else: ?>

                <?php foreach ($_disabled as $_drel => $_dv):
                  $_dname = str_replace('_', ' ', strtolower(preg_replace('/_SCRIPTS$/', '', $_dv['array'] ?? '')));
                ?>

                    <?= htmlspecialchars(basename($_drel, '.sh')) ?>
                    <?= htmlspecialchars($_dname) ?>

                <?php endforeach; ?>

              <?php endif; ?>

              ▾
              Fallback

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.

              ▸
              How it works

Varaverk owns the cron layer. schedule.json stores every orchestrator's enabled state, cron expression, and log setting. When you save, the plugin rebuilds /etc/cron.d/varaverk — one entry per enabled orchestrator. That's all cron knows about.

Orchestrators are the only scripts with cron schedules. Each one reads its SCRIPTS list out of master.conf and runs each child in sequence. The children do the actual work. The orchestrator controls the order and the timing. A child failure stops the chain.

master.conf is the per-host control plane. Comment out a line to disable that script on this host without touching any code. Both servers run the same repository — different master.conf comments produce different behaviour per machine.

Children only get their own cron when their orchestrator is OFF. When the orch is on, it owns the timing completely.

common.sh provides the shared runtime every script sources: acquire_lock(), log_msg(), info(), success(), warn(), error(). Lock files live in /tmp/varaverk_locks/. acquire_lock() exits cleanly if a prior run is still in progress — nothing piles up.

Logs are written per script to the appdata path. The Log button shows the latest run. Verbose mode passes --log for per-step detail.

Array events bypass cron entirely. array_start fires when Unraid brings the array up; array_stop fires before shutdown. array_started handles all post-start setup in sequence; array_stopping handles the graceful teardown.

The watchdog tier runs every 15 minutes as a single-pass chain: resource_watchdog frees memory pressure first, docker_watchdog restarts down containers into that headroom, system_watchdog checks storage/WebGUI/network, stability_watchdog reboots as a last resort. The order matters — restarting containers before freeing memory just restarts them into the same pressure.

              ▸
              This started as...

One script. A few lines — checking whether a container was running and restarting it if not. Backgrounded through User Scripts, sleeping a minute between checks, essentially forgotten. It did its job.

Then a conversation with a buddy: wouldn't it be cool if it emailed when something crashed? Sure, added it. Wouldn't it be cool if it handled more containers? Easy — grow the list. Wouldn't it be cool if both servers stayed in sync automatically so neither of us had to remember to do it manually?

That one took a while.

Order started to matter. Freeing memory before attempting container restarts meant the containers actually stayed up. Sync had to happen before the media database refresh, not after. Watchdogs couldn't block each other. The big loop that tried to do everything started breaking into pieces — one piece per job, and a sequencer that knew which piece went first.

master.conf became the control plane: one file, two different sets of comments for two different hosts. Same codebase, different behaviour per machine. The orchestrator pattern came out of not wanting to duplicate logic.

By the time the User Scripts plugin couldn't clearly show what was actually scheduled, whether something was running, or why a sync had quietly stopped working two weeks ago — the interface became the next thing to build.

Varaverk started as a status table and a schedule editor. The scheduler became the main thing. The monitor followed.

Still the same two servers, two households, the same media stack running itself. The scripts just grew up around it.

Docs

          <?php if ($_mainReadme || !empty($_childReadmes)): ?>

              <?php if (!empty($_childReadmes)): ?>
                ▸
              <?php else: ?>

              <?php endif; ?>
              README

            <?php if (!empty($_childReadmes)): ?>

              <?php foreach ($_childReadmes as $_cr):
                $_crLabel = preg_replace('/^README-?/i', '', basename($_cr));
                $_crLabel = preg_replace('/\.md$/i', '', $_crLabel);
                $_crLabel = str_replace('_', ' ', $_crLabel) ?: dirname($_cr);
              ?>
              <div class="vv-sb-row vv-sb-child-row"
                   data-id="<?= htmlspecialchars($_cr) ?>"
                   data-hdr=""
                   data-desc=""
                   onclick="vvSelectScript(this)">
                └
                <?= htmlspecialchars($_crLabel) ?>

              <?php endforeach; ?>

            <?php endif; ?>

          <?php endif; ?>

          <?php if ($_mainManual || !empty($_childManuals)): ?>

              <?php if (!empty($_childManuals)): ?>
                ▸
              <?php else: ?>

              <?php endif; ?>
              Manual

            <?php if (!empty($_childManuals)): ?>

              <?php foreach ($_childManuals as $_cm):
                $_cmLabel = preg_replace('/^Manual-?/i', '', basename($_cm));
                $_cmLabel = preg_replace('/\.md$/i', '', $_cmLabel);
                $_cmLabel = str_replace('_', ' ', $_cmLabel) ?: dirname($_cm);
              ?>
              <div class="vv-sb-row vv-sb-child-row"
                   data-id="<?= htmlspecialchars($_cm) ?>"
                   data-hdr=""
                   data-desc=""
                   onclick="vvSelectScript(this)">
                └
                <?= htmlspecialchars($_cmLabel) ?>

              <?php endforeach; ?>

            <?php endif; ?>

          <?php endif; ?>

Orch

          <?php foreach ($tree as $orch):
            $orchId   = $orch['id'];
            $orchHdr  = vv_script_header_clean(SCRIPTS_DIR . '/' . $orchId);
            $hasKids  = !empty($orch['children']);
          ?>

            <div class="vv-sb-row vv-sb-orch-row"
                 data-id="<?= htmlspecialchars($orchId) ?>"
                 data-hdr="<?= htmlspecialchars($orchHdr) ?>"
                 data-desc="<?= htmlspecialchars($orch['desc']) ?>"
                 onclick="vvSelectScript(this)">
              <?php if ($hasKids): ?>
                ▸
              <?php else: ?>

              <?php endif; ?>
              <?= htmlspecialchars($orch['label']) ?>
              <?php if (str_starts_with($orch['cron'], 'array_')): ?>
                ⚡
              <?php elseif ($orch['suggested_cron']): ?>
                <?= htmlspecialchars($orch['suggested_cron']) ?>
              <?php endif; ?>
              <?php
                $_os = @json_decode(@file_get_contents(vv_job_stat_path($orchId)), true);
                $_or = $_os['status'] ?? '';
                $_oc = $_or === 'ok' ? 'vv-stat-ok' : ($_or === 'warn' ? 'vv-stat-warn' : ($_or === 'error' ? 'vv-stat-error' : ($_or === 'skipped' ? 'vv-stat-skip' : ($orch['enabled'] ? 'vv-sug-on' : 'vv-stat-none'))));
              ?>
              <span class="<?= $_oc ?> vv-sb-status" title="<?= $_or ? htmlspecialchars("last: $_or") : ($orch['enabled'] ? 'enabled' : 'never run') ?>">
                <?= ($_or || $orch['enabled']) ? '●' : '○' ?>

            <?php if ($hasKids): ?>

              <?php foreach ($orch['children'] as $child):
                $childHdr = vv_script_header_clean(SCRIPTS_DIR . '/' . $child['id']);
              ?>
              <div class="vv-sb-row vv-sb-child-row"
                   data-id="<?= htmlspecialchars($child['id']) ?>"
                   data-hdr="<?= htmlspecialchars($childHdr) ?>"
                   data-desc="<?= htmlspecialchars($child['desc']) ?>"
                   onclick="vvSelectScript(this)">
                └
                <?= htmlspecialchars($child['label']) ?>
                <?php if (!empty($child['suggested_cron'])): ?>
                  <?= htmlspecialchars($child['suggested_cron']) ?>
                <?php endif; ?>
                <?php
                  $_cs = @json_decode(@file_get_contents(vv_job_stat_path($child['id'])), true);
                  $_cr = $_cs['status'] ?? '';
                  $_cc = $_cr === 'ok' ? 'vv-stat-ok' : ($_cr === 'warn' ? 'vv-stat-warn' : ($_cr === 'error' ? 'vv-stat-error' : ($_cr === 'skipped' ? 'vv-stat-skip' : (!empty($child['conf_enabled']) ? 'vv-sug-on' : 'vv-stat-none'))));
                ?>
                <span class="<?= $_cc ?> vv-sb-status" title="<?= $_cr ? htmlspecialchars("last: $_cr") : (!empty($child['conf_enabled']) ? 'enabled' : 'never run') ?>">
                  <?= ($_cr || !empty($child['conf_enabled'])) ? '●' : '○' ?>

              <?php endforeach; ?>

            <?php endif; ?>

          <?php endforeach; ?>

Repository

          <?php if (!empty($_repoFiles)): ?>

              ▸
              Varaverk

              <?php
              $_rootFiles = array_values(array_filter($_repoFiles, function($f) { return strpos($f, '/') === false; }));
              $_subFiles  = array_values(array_filter($_repoFiles, function($f) { return strpos($f, '/') !== false; }));
              // Root-level files under Varaverk/ divider
              if (!empty($_rootFiles)):
              ?>

Varaverk/

              <?php foreach ($_rootFiles as $_rel):
                $_isScript = (bool)preg_match('/\.sh$/i', $_rel);
                $_hdr      = $_isScript ? vv_script_header_clean(SCRIPTS_DIR . '/' . $_rel) : '';
              ?>
              <div class="vv-sb-row"
                   data-id="<?= htmlspecialchars($_rel) ?>"
                   data-hdr="<?= htmlspecialchars($_hdr) ?>"
                   data-desc=""
                   onclick="vvSelectScript(this)"
                   style="padding-left:18px">

                <span class="vv-sb-name" style="color:<?= $_isScript ? '#b0c4d0' : '#7a9eb5' ?>;font-size:12px;">
                  <?= htmlspecialchars($_rel) ?>

              <?php endforeach; endif; ?>
              <?php
              // Subdirectory files
              $_seenDirs = [];
              foreach ($_subFiles as $_rel):
                $_parts    = explode('/', $_rel);
                $_dirParts = array_slice($_parts, 0, -1);
                $_filename = end($_parts);
                $_isScript = (bool)preg_match('/\.sh$/i', $_filename);
                for ($_di = 0; $_di < count($_dirParts); $_di++):
                  $_dirPath = implode('/', array_slice($_dirParts, 0, $_di + 1));
                  if (!in_array($_dirPath, $_seenDirs)):
                    $_seenDirs[] = $_dirPath;
              ?>
              <div class="vv-info-divider" style="padding-left:<?= $_di * 14 + 8 ?>px;font-size:10px;">
                <?= htmlspecialchars($_dirParts[$_di]) ?>/

              <?php
                  endif;
                endfor;
                $_depth = count($_dirParts);
                $_hdr   = $_isScript ? vv_script_header_clean(SCRIPTS_DIR . '/' . $_rel) : '';
              ?>
              <div class="vv-sb-row"
                   data-id="<?= htmlspecialchars($_rel) ?>"
                   data-hdr="<?= htmlspecialchars($_hdr) ?>"
                   data-desc=""
                   onclick="vvSelectScript(this)"
                   style="padding-left:<?= $_depth * 14 + 4 ?>px">

                <span class="vv-sb-name" style="color:<?= $_isScript ? '#b0c4d0' : '#7a9eb5' ?>;font-size:12px;">
                  <?= htmlspecialchars($_filename) ?>

              <?php endforeach; ?>

          <?php endif; ?>

            Name:
            <input type="text" id="vv-editor-name" class="vv-cron" style="flex:1" placeholder="script_name  (no .sh)">

              ›
              <input type="text" id="vv-find-input" placeholder="Find…"
                     oninput="vvFindSearch()" onkeydown="vvFindKeydown(event)">

              <button class="vv-btn-sm vv-find-nav-btn" onclick="vvFindNav(-1)" title="Previous (Shift+Enter)">↑</button>
              <button class="vv-btn-sm vv-find-nav-btn" onclick="vvFindNav(1)"  title="Next (Enter)">↓</button>
              ×

              <input type="text" id="vv-replace-input" placeholder="Replace with…"
                     onkeydown="vvReplaceKeydown(event)">
              <button class="vv-btn-sm vv-repl-btn" onclick="vvReplaceOne()" title="Replace (Enter)">Replace</button>
              <button class="vv-btn-sm vv-repl-btn" onclick="vvReplaceAll()" title="Replace All">All</button>

            Go to line
            <input type="number" id="vv-goto-input" min="1" placeholder="#"
                   oninput="vvGotoApply()" onkeydown="vvGotoKeydown(event)">

            ×

              ▾
              Keyboard Shortcuts

Ctrl+S — save

Ctrl+Z / Ctrl+Y — undo / redo (200 steps)

Ctrl+/ — toggle line comment

Tab / Shift+Tab — indent / dedent

Ctrl+F — find

Ctrl+H — find & replace

Ctrl+G — go to line

Ctrl+D — select next occurrence

Ctrl+L — select line

Ctrl+Shift+K — delete line

Alt+↑/↓ — move line

Alt+Shift+↓ — duplicate line below

Home — smart (first non-space → col 0)

( { [ " ` — auto-close pair; wraps selection; Backspace deletes both


Now I need to find `vv_tools_scripts()` in scheduler.php:


grep -n "vv_tools_scripts\|function vv_tools" /boot/config/plugins/varaverk/Plugin/unraid/include/scheduler.php

Find vv_tools_scripts function definition


233:function vv_tools_scripts(): array {


Conversation compacted