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/ or Plugin/*/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; }));


Let me continue reading the remaining pages.


1	<?php
2	$_myHost  = vv_detect_host();
3	$_vars    = vv_conf_vars();
4	$_myId    = strtoupper($_myHost);
5
6	// Storage
7	$_confKey = $_myId . '_STORAGE_MODE_INTERNAL';
8	$_confVal = $_vars[$_confKey] ?? null;
9
10	// Notifications
11	$_notifyUnraid  = ($_vars['NOTIFY_UNRAID']  ?? 'true')  !== 'false';
12	$_enableLogging = ($_vars['ENABLE_LOGGING'] ?? 'false') === 'true';
13	$_discordKey    = $_myId . '_DISCORD_WEBHOOK';
14	$_discordHook   = $_vars[$_discordKey] ?? '';
15
16	// API key
17	$_apiKeyVar  = $_myId . '_UNRAID_API_KEY';
18	$_apiKey     = $_vars[$_apiKeyVar] ?? '';
19	$_apiPreview = $_apiKey ? substr($_apiKey, 0, 8) . '...' . substr($_apiKey, -4) : '';
20	?>
21
62
63

64
65
66

67

68	      Storage Location
69	      …
70

71
72
Loading…

73
74

75
76

77	      Varaverk stores all scripts, configuration, state files, and the git repository in
78	      SCRIPTS_DIR.
79

80	      Internal NVMe/SSD — direct access, zero write-wear concern, git pull/push from boot volume.

81	      USB Flash — preserves flash lifetime. Requires array to be started. git pull syncs
82	      Plugin/
83	      back to /boot/
84	      after each pull so the webUI always stays current.
85

86
87

88	      Array must be started for Varaverk to function in flash mode. The plugin tab is always accessible.
89

90

91	      Array is not started — cannot migrate to flash mode right now. Start the array first.
92

93
94

95	      <button id="vv-stor-migrate-btn" class="vv-set-btn" onclick="vvStorMigrate()" style="display:none;"></button>
96	      <button class="vv-set-btn" onclick="vvStorLoad()" style="font-size:10px;padding:4px 10px;">↻ Refresh</button>
97
98

99
100

101

102
103
104

105
Notifications

106
107

108
109	        <div class="vv-set-tog-track<?= $_notifyUnraid ? ' on' : '' ?>" id="vv-ntf-unraid"
110	             onclick="vvNtfToggle(this,'NOTIFY_UNRAID')">

111

112
Unraid native notifications

113
Appears in the Unraid bell icon — job completions, warnings, errors

114

115
116
117	        <div class="vv-set-tog-track<?= $_enableLogging ? ' on' : '' ?>" id="vv-ntf-logging"
118	             onclick="vvNtfToggle(this,'ENABLE_LOGGING')">

119

120
Verbose logging

121
Adds detailed [LOG] lines to script output — useful for debugging scheduled runs

122

123
124

125
126

127
128

129

130	        Discord webhook
131	        <?= htmlspecialchars($_discordKey) ?>
132

133

134	        <input class="vv-set-inp" id="vv-ntf-discord" type="text"
135	               placeholder="https://discord.com/api/webhooks/…"
136	               value="<?= htmlspecialchars($_discordHook) ?>">
137	        <button class="vv-set-btn primary" onclick="vvNtfSaveWebhook()" id="vv-ntf-save"
138	                style="white-space:nowrap;flex-shrink:0;">Save</button>
139

140
Leave blank to disable Discord notifications for this host

141

142
143

144
145
146

147

148	      Unraid API Keys
149	      …
150

151
152

153
154

155	      Each host registers
156	      Varaverk_HOST1
157	      in every machine's local Unraid registry — own machine plus all partners.
158	      The key values are stored in the private host*.conf
159	      so each host can query every other host's GraphQL API directly — real-time monitoring, no SSH.
160	      During offboard, partner keys are removed from each registry automatically.
161

162
163

164	      <button class="vv-set-btn" onclick="vvApiCheck()" id="vv-api-check-btn">↻ Check status</button>
165	      <button class="vv-set-btn" onclick="vvApiRenew(false)" id="vv-api-local-btn">Renew local</button>
166	      <button class="vv-set-btn primary" onclick="vvApiRenew(true)" id="vv-api-all-btn">Setup all host keys</button>
167
168

169

170

171
172

173
174


1	<?php
2	// First-run setup wizard — uniform flow for all hosts.
3	// Step 1: auto-detect environment + server identity form.
4	// Step 2: auto-populate + guide + checklist.
5	// master.conf pull (for partner servers) lives in the checklist, not here.
6
7	$detectedHostname = vv_get_hostname();
8	?>
9	<link rel="stylesheet" href="/plugins/varaverk/css/varaverk.css">
10
78
79

80
81
⬡ Varaverk — First Run

82
Set up this server before the plugin can start.

83
84
85

86
87

Detecting environment…

88
89

90	      This server's hostname
91	      <input type="text" id="vv-hostname" value="<?= htmlspecialchars($detectedHostname) ?>" autocomplete="off" spellcheck="false">
92
Must match Unraid Settings → Identification exactly (case-sensitive)

93

94
95

96	    Server role
97

98

99	        Primary
HOST1 · first server
100

101

102	        Partner
HOST2+ · joining primary
103

104

105
106

107

108	        Partner's hostname (optional — can fill in later)
109	        <input type="text" id="vv-partner-hostname" value="" placeholder="unRAID-PartnerServer" autocomplete="off" spellcheck="false">
110

111

112
113

114

115	        Primary server's hostname required
116	        <input type="text" id="vv-primary-hostname" value="" placeholder="unRAID-PrimaryServer" autocomplete="off" spellcheck="false">
117

118

119	        Your slot
120	        <select id="vv-partner-slot">
121	          <option value="host2">HOST2</option>
122	          <option value="host3">HOST3</option>
123	          <option value="host4">HOST4</option>
124	        </select>
125

126

127	        SSH key and master.conf pull are handled automatically after save.
128

129

130
131	    <button class="vv-btn" id="vv-main-btn" onclick="vvDoSave()">Save and continue →</button>
132

133

134
135
136

137

138
Step 2 of 2

139
140
⟳ Running auto-populate…

141
142

143	      Quick start
144

145
Create your Unraid API key below — needed for live monitor stats

146
Open Scheduler → Edit host.conf — only three things need manual entry:

147
148	              EMBY_API_KEY — Emby Dashboard → API Keys → + New Key

149	              DISCORD_WEBHOOK — for notifications (optional)

150	              DAILY_SYNC_SHARES — media paths to rsync nightly

151	              Everything else was auto-populated or has working defaults
152

153
If partnering: the checklist below will guide you through pulling HOST1's config and running onboard

154

155

156
157

158	      <button id="vv-key-btn" onclick="vvCreateKey(this)" class="vv-btn" style="flex:1;background:#1a3a1a;border-color:#2e6b2e;color:#6fcf97;">
159	        Create API Key
160	      </button>
161	      Skip →
162

163

164
165

166
Setup checklist

167

Loading…

168
169

170	      Go to Scheduler →
171

172

173
174

175
176


1
43
44

45	  Watchdog
46
47

48
49

50
Loading…

51

52
53


Now I have examined all 11 PHP pages and the CSS file. Let me compile a comprehensive audit report with all findings.

## COMPREHENSIVE AUDIT: Varaverk Unraid Plugin Pages

### FILE INVENTORY
**11 PHP Pages Reviewed:**
1. arrs.php
2. auth.php
3. docker.php
4. fallback.php
5. monitor.php (partial - very large file)
6. partnership.php
7. rsync.php
8. scheduler.php
9. settings.php
10. setup.php
11. watchdog.php

**CSS File:** varaverk.css (947 lines, well-organized shared stylesheet)

---

## 1. VISUAL INCONSISTENCIES

### Tab/Button Styling Divergence

**ISSUE: arrs.php uses non-standard toggle styling**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/arrs.php` (lines 71-76)
- Uses custom `.vv-arr-tog-track` (28px width, 16px height) with inline `onclick` handlers
- **REFERENCE CORRECT VERSION:** docker.php and watchdog.php use `.vv-toggle` from varaverk.css (36px width, 20px height)
- CSS defines standardized toggle at varaverk.css:733-740
- **FIX:** Replace arrs.php toggle HTML/CSS with standard `.vv-toggle` pattern from other pages

**ISSUE: auth.php uses distinct modal system**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/auth.php` (lines 88-91)
- Modal overlay at lines 229-231 with `.vv-au-overlay` and `.vv-au-modal`
- Other pages (partnership, rsync) use `.vv-card` pattern in modal-like containers
- **NOTE:** This is acceptable if modals are auth-specific, but overlay color (#0009) differs from varaverk.css patterns
- **REFERENCE:** varaverk.css doesn't define a global modal system—each page rolls its own

**ISSUE: Buttons lack unified color coding**
- arrs.php: Uses inline `style` attribute for button colors (lines 262-276, 381-382)
- docker.php: Uses `.vv-dk-btn`, `.vv-dk-btn.active`, `.vv-dk-btn.warn`, `.vv-dk-btn.prim` (CSS lines 2-9)
- scheduler.php: Uses `.vv-btn-sm`, `.vv-run-btn`, `.vv-dry-btn`, `.vv-log-btn`, `.vv-edit-btn` (varaverk.css:121-312)
- partnership.php: Uses `.vv-pt-action-btn.run`, `.vv-pt-action-btn.warn`, `.vv-pt-action-btn.info` (lines 20-24)
- **FIX:** Consolidate button classes in varaverk.css; many pages duplicate button styling inline

### Spacing & Layout

**ISSUE: Card padding inconsistency**
- arrs.php: `.vv-arr-card { padding: 12px 14px; }` (line 3)
- monitor.php: `.vv-card { padding: 12px; }` (varaverk.css:29)
- auth.php: `.vv-au-card { border-radius: 6px; overflow: hidden; }` — no explicit padding (line 41)
- watchdog.php: `.vv-wd-card { padding: 10px; }` (line 2)
- **INCONSISTENCY:** 10px, 12px, and 12px 14px (horizontal 14px), no unified standard
- **REFERENCE CORRECT:** Use 12px across all pages (varaverk.css standard)

**ISSUE: Icon/badge sizing varies widely**
- arrs.php: State dot `.vv-arr-dot { width: 7px; height: 7px; }` (line 9)
- watchdog.php: `.vv-wd-dot { width: 6px; height: 6px; }` (line 22)
- partnership.php: `.vv-pt-dot { width: 8px; height: 8px; }` (line 18)
- docker.php: No explicit dot class, uses inline styles
- **FIX:** Define single `.vv-status-dot` or `.vv-dot` class in varaverk.css (likely 7px—most common)

### Header/Title Typography

**ISSUE: h3 styling scattered across inline styles vs CSS**
- varaverk.css (31-34): `.vv-card h3` defines standard — uppercase, #888 color, 13px font-size, letter-spacing
- auth.php: `.vv-au-modal h3` at line 92 duplicates these rules inline (uppercase, 12px, #888)
- docker.php: No h3 class; uses `<span class="vv-dk-title">` (line 3) instead
- **ISSUE:** Some pages bypass `.vv-card h3` by using non-h3 elements or redefining
- **FIX:** Enforce `.vv-card h3` usage; audit all pages for h3 and replace with consistent pattern

### Color Token Inconsistencies

**ISSUE: Same semantic colors have different hex values**
- Accent green OK: `#4caf50` (varaverk.css), `#6fcf97` (arrs.php line 212, docker.php line 35), `#6aaa6a` (arrs.php line 945)
- Error red: `#f44336` (varaverk.css), `#ef5350` (arrs.php line 206), `#ef9a9a` (auth.php line 354)
- Warning orange: `#ff9800` (varaverk.css), `#ffb74d` (auth.php line 354), `#aa7020` (partnership.php line 123)
- **IMPACT:** Severely fragmented color palette. Primary colors should be CSS variables or constants
- **FIX:** Create CSS color variables (--color-success, --color-error, --color-warn) and use throughout

---

## 2. UX GAPS

### Missing Loading States

**ISSUE: arrs.php has no loading indicator while fetching**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/arrs.php` (lines 411-417)
- `vvArrsLoad()` makes fetch call but initial grid shows "Loading…" forever if API fails silently
- **REFERENCE CORRECT:** monitor.php shows "Loading…" in grid (line 11, line 15) during initial page load
- **FIX:** Add error state: if fetch fails, show "Error loading arrs — check API" in grid with retry button

**ISSUE: auth.php modals have no busy states on buttons**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/auth.php` (lines 456-461)
- When user clicks "Save" button in proxy modal, button text doesn't change to "Saving…"
- User can click multiple times, spawning duplicate requests
- **REFERENCE CORRECT:** scheduler.php shows loading: buttons get `disabled=true` and text changes (scheduler lines ~1600+)
- **FIX:** Add `btn.disabled = true; btn.textContent = 'Saving…'` before fetch; restore on completion

**ISSUE: docker.php folder rename has no in-flight feedback**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php` (lines 289-295)
- Input blur handler makes fetch but input field stays active with old text until response
- User can't tell if rename is pending
- **FIX:** Add grey text color or opacity 0.6 while fetch is in flight

### Missing Empty States

**ISSUE: rsync.php bandwidth history shows "No data" but not for all sections**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php` (lines 671-675)
- `_bwSection()` returns empty card if no history, but window cards (critical/daily/weekly) may be empty with no message
- **FIX:** Add empty state message to each window card: "No runs in history" or "Not configured"

**ISSUE: scheduler.php info panels have placeholders but no real empty states**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php` (lines 479-551)
- "Next Runs", "Recent Activity", "Recent Errors" all show "loading…" but no fallback for "No data"
- If no errors exist, should show "All clear" with icon
- **FIX:** Add real empty state HTML: `<div class="vv-board-placeholder">All clear ✓</div>`

### Missing Confirmation on Destructive Actions

**ISSUE: auth.php user/group deletion uses confirm() but inconsistently**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/auth.php` (line 496, 656, 745, 746)
- Uses browser `confirm()` dialog—works but looks jarring in modern UI
- **REFERENCE:** partnership.php (lines 191, 201, 231) uses same pattern
- **UX PROBLEM:** No inline confirmation modal that matches the design language
- **FIX:** Create a reusable modal confirm UI (CSS-only, no browser dialog) matching `.vv-au-overlay` pattern

**ISSUE: docker.php folder delete only uses single confirm()**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php` (line 304)
- `confirm('Delete "${f.name}"?')` — but consequence (containers ungrouped) not explained
- User might think containers are deleted, not just ungrouped
- **FIX:** Change message: `'Delete folder "${f.name}"?\n\nContainers will be moved to Ungrouped.'`

**ISSUE: scheduler.php delete script button (#vv-delete-script-btn) has no confirmation**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php` (line 370)
- Button is hidden by default, but when shown, onclick="vvDeleteScript()" doesn't show confirm dialog
- Destructive action (deletes custom script file) with no "are you sure?"
- **FIX:** Add confirm dialog in `vvDeleteScript()` JavaScript function

### Missing Error Feedback

**ISSUE: fallback.php has no error state for API failure**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/fallback.php` (lines 232-237)
- `vvFbLoad()` fetch `.catch(() => {})` silently ignores errors
- If API unreachable, grid shows "Loading…" forever—no error message
- **FIX:** Show error card: `<div>Error loading fallback data — check API</div>` on fetch failure

**ISSUE: partnership.php settings load has no error UI**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/partnership.php` (lines 494-512)
- `vvPtLoadSettings()` shows error HTML but format is bare: `<div style="color:#f88;...">Error: ...</div>`
- Uses `#f88` (not standard error color) and no error icon
- **FIX:** Use `.vv-au-err` class with proper error styling; add icon (⚠ or ✗)

### Missing Status Indicators

**ISSUE: rsync.php manual sync has no "in progress" visual**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php` (lines 234-245)
- Run button disables but output pre stays hidden until fetch completes
- User doesn't know sync is running until output appears
- **REFERENCE CORRECT:** scheduler.php shows dot spinner during run
- **FIX:** Add pulsing indicator or "Running…" text that appears immediately on button click

**ISSUE: docker.php "Edit folders" mode has no mode indicator**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php` (lines 362-367)
- Button text toggles between "Edit folders" / "Done editing" (`.active` class)
- But page title doesn't change—user might not notice edit mode is active
- **FIX:** Add banner at top of grid: `<div class="vv-banner">Drag containers between folders. Click Save when done.</div>`

---

## 3. INLINE STYLES VS CSS

### Critical Finding: Massive CSS Duplication Across Pages

**CSS in varaverk.css: 947 lines (primary stylesheet)**

**Inline `<style>` blocks by page:**

1. **arrs.php** (lines 1-85): 85 lines
   - Duplicates: `.vv-card` padding, `.vv-row` layout, card styling
   - NEW styles: `.vv-arr-hdr`, `.vv-arr-stats`, `.vv-arr-disk-*`
   - **SCORE:** 40% shared, 60% new. ACCEPTABLE—arrs has unique card structure

2. **auth.php** (lines 1-120): 120 lines
   - Duplicates: button colors, modal overlay (basic), toggle switches
   - Unique: `.vv-au-badge` (many color variants), `.vv-au-tbl` (table styling)
   - **SCORE:** 30% shared, 70% new. **PROBLEM: Modal should be in varaverk.css as reusable component**

3. **docker.php** (lines 1-74): 74 lines
   - Duplicates: button colors, card styling
   - Unique: `.vv-dk-folder` (folder card layout), `.vv-dk-ctr` (container row), popover menu
   - **SCORE:** 20% shared, 80% new. ACCEPTABLE—docker layout is unique

4. **fallback.php** (lines 1-34): 34 lines
   - Duplicates: card styling, badge colors, state dots
   - Unique: `.vv-fb-active` (active state banner), `.vv-fb-tier-*` (tier display)
   - **SCORE:** 50% shared, 50% new. MIXED—could consolidate badge colors

5. **partnership.php** (lines 1-57): 57 lines
   - Duplicates: card styling, action button colors, toggle switches
   - Unique: `.vv-pt-node` (node card), `.vv-pt-action-btn.run/warn/info` (action buttons)
   - **SCORE:** 30% shared, 70% new. **PROBLEM: Action button variants should be CSS base with modifiers**

6. **rsync.php** (lines 1-117): 117 lines
   - Duplicates: card styling, button colors, toggle styling
   - Unique: `.vv-ry-toggle-*`, `.vv-ry-win-*` (window cards), profile history styles
   - **SCORE:** 20% shared, 80% new. ACCEPTABLE—rsync windows are unique layout

7. **scheduler.php** (lines 0): **NO `<style>` tag—EXCELLENT**
   - Purely relies on varaverk.css
   - Uses `.vv-btn-sm`, `.vv-run-btn`, `.vv-dry-btn`, etc. from CSS
   - **SCORE:** 100% CSS file. **BEST PRACTICE**

8. **settings.php** (lines 1-61): 61 lines
   - Duplicates: card styling, button colors, toggle switches
   - Unique: `.vv-set-tog-*` (toggle styling), badge variants
   - **SCORE:** 40% shared, 60% new. MIXED

9. **setup.php** (lines 1-77): 77 lines
   - NO duplication of varaverk.css—this is first-run wizard, self-contained
   - Unique: entire layout is custom (setup card, role buttons, detection banner)
   - **SCORE:** 0% shared, 100% new. ACCEPTABLE—intentionally isolated UI

10. **watchdog.php** (lines 1-42): 42 lines
    - Duplicates: card styling, badge colors, bar styling
    - Unique: `.vv-wd-*` pressure card, strike display
    - **SCORE:** 40% shared, 60% new. MIXED

### CSS Organization in varaverk.css

**STRENGTHS:**
- Well-commented section headers (lines with `/* ── comment ── */`)
- Logical grouping: tabs (6-9), cards (28-30), buttons (121-312), editor (338-464), scheduler (517-687)
- Responsive design properly cascaded (media queries at end, 189-282)
- Single-responsibility classes (`.vv-banner-ok`, `.vv-banner-warn`, not `.vv-banner-success`)

**WEAKNESSES:**
- Color values hardcoded everywhere; no CSS variables (e.g., `#4caf50` appears 50+ times)
- No comment block for "BUTTON STYLES" section—section just starts at line 121
- Button variants split: `.vv-btn-sm` (121-123) vs `.vv-run-btn` (292) vs page-specific `.vv-au-btn` (auth.php)
- No documented component examples or usage guide

### RECOMMENDATION

**CONSOLIDATE TO 3 CATEGORIES:**

1. **Base varaverk.css** (keep as-is): 947 lines, shared framework
2. **Page-specific `<style>` blocks** (acceptable):
   - docker.php, rsync.php, arrs.php (unique layouts)
   - setup.php (isolated first-run flow)
   - Keep under 100 lines each; no duplication of button/card colors
3. **MOVE TO varaverk.css** (extract):
   - All modal styling from auth.php (`.vv-au-overlay`, `.vv-au-modal`)—make reusable `.vv-modal-*`
   - All button color variants (consolidate `.vv-run-btn`, `.vv-pt-action-btn.run`, `.vv-dk-btn.prim`)
   - Toggle switch styling (currently 4 different toggle implementations)

---

## 4. BROKEN OR INCOMPLETE FLOWS

### Unfinished Implementations

**ISSUE: arrs.php "Fetch now" button appears incomplete**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/arrs.php` (lines 262-264, 274-276)
- Shows "↻ Fetch now" button for remote nodes with no cache data
- Button calls `vvArrsRefreshRemote()` (line 422) and reloads data
- **PROBLEM:** No feedback while fetching—button doesn't disable or show "Fetching…"
- **FIX:** Add `btn.disabled = true; btn.textContent = '↻ Fetching…'` in `vvArrsRefreshRemote()`

**ISSUE: docker.php popover menu positioning is hardcoded**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/docker.php` (lines 330-332)
- ```javascript
  pop.style.left = Math.min(x, vw-180)+'px';
  pop.style.top  = Math.min(y+8, vh-180)+'px';
  ```
- Assumes popover is 180px—if content grows, positioning breaks
- **FIX:** Calculate actual popover dimensions or use CSS `position: fixed; max-width: 200px`

**ISSUE: scheduler.php cron field validation shows red border but no error message**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php` (lines 98-100)
- Cron input has `onblur="vvSaveCronBlur(this)"` but JavaScript (not included in excerpt) doesn't show error tooltip
- User sees red border but doesn't know what's wrong
- **FIX:** Add title attribute or inline error message below field

**ISSUE: partnership.php "Phase 2" buttons are disabled but visually unclear**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/partnership.php` (lines 1085-1086)
- Re-run Phase 2 button has `style="opacity:.35;..."` (dimmed)
- But button is still clickable—just disabled via `onclick` logic
- User might think button is broken, not just "already done"
- **FIX:** Add `disabled` attribute or replace opacity with grey color scheme

### Incomplete State Management

**ISSUE: rsync.php profile editor loses form state on cancel**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/rsync.php` (lines 1036-1039)
- "Cancel" button reverts to view mode but doesn't reset unsaved form changes
- User might lose work if they accidentally switch modes
- **NOTE:** This is acceptable if user intends to lose changes; confirm on cancel

**ISSUE: auth.php user/group modal doesn't validate input before save**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/auth.php` (lines 556-576)
- Modal has fields for username, display name, email
- Email field is required but form doesn't check `.validity` or run HTML5 validation
- User can submit blank email, gets server error with no clear feedback
- **FIX:** Add `required` attribute to inputs; validate on save button click with visual feedback

### Placeholder Text

**ISSUE: setup.php first-run shows "Loading…" in detection banner**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/setup.php` (line 87)
- Fetches `/plugins/varaverk/api/setup.php?action=detect` but if API is slow, user sees "Detecting environment…" for too long
- No timeout—if API never responds, banner never updates
- **FIX:** Add 5s timeout; show "Detection unavailable" if timeout

**ISSUE: scheduler.php suggestions show "calculating…" placeholder**
- File: `/boot/config/plugins/varaverk/Plugin/unraid/pages/scheduler.php` (line 480)
- Next Runs section shows `<div class="vv-board-placeholder">calculating…</div>`
- If JavaScript doesn't run or fails, placeholder stays visible
- **FIX:** Replace with real empty state after initial load, not placeholder

---

## 5. CROSS-PAGE DUPLICATION

### Duplicated JavaScript Functions

**Toggle switch onclick pattern (repeated 5 times):**
- arrs.php line 345: `vvArrToggle(this, 'KEY', file)`
- auth.php line 427-429: similar toggle logic (inline in modal builder)
- docker.php line 265: `onclick="event.stopPropagation();"`
- partnership.php line 298-316: `vvPtToggleSync(el, varName, enabled)`
- settings.php line N/A: uses similar pattern

**CONSOLIDATION OPPORTUNITY:** Create single `vvToggle(element, key, file, onSuccess)` in shared JS file

**Color formatting functions (duplicated):**
- arrs.php lines 100-117: `_rel()`, `_n()`, `_gb()`, `_tb()`, `_sz()` (size formatters)
- docker.php lines 116-119: `_shortImage()` (regex strip)
- fallback.php lines 48-62: `_dur()`, `_fmtDelay()` (duration formatters)
- partnership.php lines 596-612: `_relTime()`, `_uptime()` (time formatters)
- rsync.php lines 554-560: `_fmtBytes()`, `_dur()` (duplicate implementations)
- watchdog.php lines 67-78: `_relTime()`, `_fmtBytes()`, `_dur()` (DUPLICATE again)

**CONSOLIDATION OPPORTUNITY:** Create `varaverk-utils.js` with shared formatters:
```javascript
// Shared utilities
function vvFormatRelTime(ts) { ... }
function vvFormatBytes(b) { ... }
function vvFormatDuration(s) { ... }
function vvFormatSize(b) { ... }
```

**Loading/error state management (duplicated with variations):**
- arrs.php line 392-396: Shows "No arrs configured" message
- docker.php line 245: Shows "No containers found"
- fallback.php line 219: Shows "No nodes configured"
- monitor.php: Shows "Loading…" in grid
- partnership.php line 226: Shows "No hosts found in master.conf"

**Pattern:** Each page reinvents "empty state" logic
**CONSOLIDATION:** Create reusable `vvShowEmptyState(container, message, icon)` function

### Duplicated HTML/CSS Patterns

**Modal overlay pattern (exact duplication):**
- auth.php lines 229-231:
```html
<div class="vv-au-overlay" id="vv-au-overlay">
  <div class="vv-au-modal" id="vv-au-modal"></div>
</div>
```
- Similar pattern in other modals but not fully standardized

**Button styling classes (MANY variants, inconsistent naming):**
- `.vv-btn-sm` (varaverk.css, scheduler)
- `.vv-run-btn` (varaverk.css, scheduler)
- `.vv-au-btn` (auth.php, local style)
- `.vv-au-btn.prim` (auth.php)
- `.vv-pt-action-btn` (partnership.php, local style)
- `.vv-pt-action-btn.run` (partnership.php)
- `.vv-dk-btn` (docker.php, local style)
- `.vv-dk-btn.prim` (docker.php)
- `.vv-ry-toggle` (rsync.php, local style)

**CONSOLIDATION:** Create unified button system in varaverk.css:
```css
.vv-btn { padding: 5px 14px; border-radius: 4px; cursor: pointer; }
.vv-btn.primary { background: #4caf50; color: #fff; }
.vv-btn.warn { background: #ff9800; color: #000; }
.vv-btn.danger { background: #f44336; color: #fff; }
```

**Table styling (duplicated):**
- auth.php lines 20-26: `.vv-au-tbl`, `.vv-au-tbl th`, `.vv-au-tbl td`
- varaverk.css lines 81-86: `#vv-docker-table`, `#vv-docker-table th`, `#vv-docker-table td`

**CONSOLIDATION:** Create `.vv-table` base class in varaverk.css; pages can extend with specific colors/widths

### Code Duplication by Page

| Page | Duplicated Functions | Duplicated CSS | Notes |
|------|---------------------|-----------------|-------|
| arrs.php | _rel, _n, _sz, _gb (5 formatters) | .vv-arr-* (good—unique) | Formatters should be shared |
| auth.php | _esc (reused in modals), modals from scratch | Button variants, modal overlay | Modal should be component |
| docker.php | _shortImage, _esc, _hidePopover | .vv-dk-* (good—unique) | Popover could be component |
| fallback.php | _dur, _fmtDelay, _activeTier | Limited duplication | Formatters should be shared |
| monitor.php | vvMeter, vvDrawChart, vvFmtBps, vvDiskRow | Heavy inline styles | Massive inline CSS (custom charts) |
| partnership.php | _relTime, _uptime, _row (3 helpers) | Button variants | Time formatters should be shared |
| rsync.php | _rel, _dur, _fmtBytes (DUPLICATE fallback.php) | Heavy inline styles | Copy-paste from fallback |
| scheduler.php | None detected (relies on CSS) | None | BEST PRACTICE |
| settings.php | No major duplication | Button styles | Good isolation |
| setup.php | No major duplication | Self-contained UI | Good isolation |
| watchdog.php | _relTime, _fmtBytes, _dur (DUPLICATES fallback + rsync) | Limited duplication | Copy-paste chain |

**EXACT DUPLICATES FOUND:**
1. `_dur()` function: fallback.php (48-54), rsync.php (546-551), watchdog.php (73-78)
2. `_relTime()` function: partnership.php (596-604), watchdog.php (58-65)
3. `_fmtBytes()` function: rsync.php (554-560), watchdog.php (67-71)

---

## 6. COMPREHENSIVE INVENTORY BY PAGE

### **arrs.php** (470 lines)
**Layout:** Grid of cards per node, sync/recovery cards, settings card
**Styling:**
- Inline `<style>` block: 85 lines (lines 1-85)
- Uses `.vv-card` from varaverk.css for outer wrapper
- Inline styles in generated HTML: e.g., line 170 `style="color:#555;"`, line 237 `dotCol`
**Issues Found:**
- Toggle switch uses custom `.vv-arr-tog-track` (not `.vv-toggle` from varaverk.css)
- Formatter functions `_rel`, `_n`, `_gb`, `_tb`, `_sz` should be shared
- No error state if API fails
- "Fetch now" button has no loading feedback
**UX Gaps:**
- Offline node shows opacity 0.6 but no "Offline" badge
- Recovery age input has no validation (accepts negative numbers)
- No confirmation before saving recovery age

---

### **auth.php** (1,022 lines)
**Layout:** 4 tabs (Proxies, Users & Groups, ACL, Certs)
**Styling:**
- Inline `<style>` block: 120 lines (lines 1-120)
- Uses `.vv-au-*` classes extensively (button, modal, badge, toggle)
- Modal overlay at lines 229-231
**Issues Found:**
- Modal system NOT in varaverk.css (should be reusable)
- Button colors duplicated from varaverk.css: `.vv-au-btn.prim` mirrors `.vv-btn-sm.active`
- No loading state on Save button in proxy modal (lines 456-461)
- User/group deletion uses browser `confirm()` not styled modal
- ACL rule editor has no inline validation
**UX Gaps:**
- Modals close immediately on success but don't show confirmation message
- No "copy to clipboard" for API key value
- Cert grid shows "Loading…" but no timeout or error fallback
- Toggle state in modal (`_togHtml()`, line 275) doesn't show immediate feedback

---

### **docker.php** (390 lines)
**Layout:** Grid with folder cards, ungrouped section, popover menu for moving containers
**Styling:**
- Inline `<style>` block: 74 lines (lines 1-74)
- Uses `.vv-dk-*` classes (unique layout)
- Popover at lines 68, lines 321-355
**Issues Found:**
- Popover positioning hardcoded to 180px (lines 330-332)—breaks if content wider
- Folder rename input blur handler makes API call but no "saving…" feedback
- Folder hover shows delete ×, but no visual confirmation before delete
- "Edit folders" mode has no visual banner indicating user is in edit mode
**UX Gaps:**
- New folder creation via `prompt()` not styled modal
- Container icon missing handler shows placeholder (line 131) but doesn't indicate why image failed
- No empty state for "Ungrouped" section if all containers are foldered
- Drift banner appears full-width but text doesn't wrap well on narrow screens

---

### **fallback.php** (243 lines)
**Layout:** Active fallback card (if in failover), per-node tier cards
**Styling:**
- Inline `<style>` block: 34 lines (lines 1-34)
- Uses `.vv-fb-*` classes (fallback-specific)
- Leverages `.vv-card` and `.vv-banner` from varaverk.css
**Issues Found:**
- `_dur()` and `_fmtDelay()` functions should be shared
- Tier pills lack hover feedback or explanation
- No error state if API fails (fetch `.catch(() => {})` at line 236 is silent)
**UX Gaps:**
- Handback strikes display uses colored dots but no legend
- "Active tier" shows numeric (1, 2, 3, 4) without labels
- No indication what "covering" means for non-partnership users

---

### **monitor.php** (1,074+ lines, partial read)
**Layout:** Grid of monitoring cards: System, UPS, CPU, Memory, Network, Scripts, Fallback, Partner, Containers, Rsync, GPU, Transcode, Streams, Watchdog, Parity, Storage, Array
**Styling:**
- NO inline `<style>` block at top (relies on varaverk.css)
- Massive inline styles in generated HTML:
  - Line 203-210: `vvMeter()` generates inline `style` for meter bars
  - Line 424-428: Memory display has inline color hex in HTML
  - Line 929-960: UPS display inline `style` attributes throughout
**Issues Found:**
- `vvMeter()` function generates inline styles instead of using CSS class (lines 203-210)
- `vvDrawChart()` uses canvas with inline colors passed as parameters
- CPU core bars use inline hex colors (line 359)
- No loading state for individual cards (all load simultaneously)
- API status banner (lines 604-621) shows inline styles with hard-coded colors
**UX Gaps:**
- No "retry" button if individual card fails to load
- API unreachable warning uses orange color but no icon
- Partner metrics missing API key shows "—" but no explanation
- Streams card has no empty state message

---

### **partnership.php** (1,268 lines, large file)
**Layout:** Config bar, host settings cards, node grid (2-4 columns), sync health card, actions card, settings accordion
**Styling:**
- Inline `<style>` block: 57 lines (lines 1-57)
- Uses `.vv-pt-*` classes (partnership-specific)
- Modal structure similar to auth.php (no separate `<style>`)
**Issues Found:**
- Action button variants `.vv-pt-action-btn.run/.warn/.info` should extend unified `.vv-btn` in varaverk.css
- Modal for editing uses styled div (lines 711-854) but pattern not standardized
- Phase 2 button (line 1085) uses `style="opacity:.35"` instead of `disabled` attribute
- Settings section uses `<style>` block (lines 37-57) for settings editing (.vv-set-*) but should share
**UX Gaps:**
- Onboard progress badges (Phase 0/1/2) lack visual numbering or progress bar
- "Test" ping button shows result in span (line 985) but text color varies (green/red)
- Delete keys panel (lines 1094-1122) is deeply nested; unclear when user is in this mode
- Settings panel accordion doesn't scroll open selected item into view

---

### **rsync.php** (1,133 lines, partial read)
**Layout:** Manual sync card (two-column path browser + flags), profile activity chart, recent runs table, settings card, info/reference panels
**Styling:**
- Inline `<style>` block: 117 lines (lines 1-117)
- Uses `.vv-ry-*` classes (rsync-specific) and `.vv-ms-*` (manual sync)
- Heavy use of inline styles in generated charts
**Issues Found:**
- `_rel()`, `_dur()`, `_fmtBytes()` are EXACT duplicates from fallback.php and watchdog.php
- Profile dropdown (line 1023) has no loading state when fetching profiles
- Manual sync output pre (line 245) hidden until fetch completes—no "running" indicator
- Browse button doesn't disable while loading (line 150)
- Flag checkboxes have hover feedback (.vv-rp-flag) but some flags missing visual distinction
**UX Gaps:**
- BW limit input accepts 0 but no explanation what "0" means (unlimited?)
- Profile history bars (lines 701-705) show relative time but no absolute units
- No confirmation before running manual rsync (could take hours)
- Recent runs table shows only last 20 rows but no "load more" or pagination

---

### **scheduler.php** (800+ lines, partial read)
**Layout:** Left: orchestrator cards with child scripts, custom scripts, tools. Right: suggestions panel (next runs, calculator, activity, errors, etc.) and log viewer
**Styling:**
- **NO inline `<style>` block** ✓ (BEST PRACTICE)
- Relies entirely on varaverk.css
- Uses `.vv-btn-sm`, `.vv-run-btn`, `.vv-dry-btn`, `.vv-log-btn`, `.vv-edit-btn`
**Issues Found:**
- Cron input has red border on invalid but no error message (line 98-100)
- Script label click (line 94) should navigate right panel but no indication user can click
- "Add Script" button requires folder name (via `prompt()`) not modal dialog
- Delete script button (line 370) has no confirmation dialog
**UX Gaps:**
- Cron calculator (lines 484-503) shows "Apply to selected" but not all cron fields support this
- Recent errors section (lines 517-527) auto-expands but no scroll-into-view
- Arrangement mode (Arrange button, line 367) has no visual banner showing mode is active
- Log search (line 399) only works on displayed log not entire history

---

### **settings.php** (173 lines)
**Layout:** Cards for storage location, notifications, API key management
**Styling:**
- Inline `<style>` block: 61 lines (lines 1-61)
- Uses `.vv-set-*` classes (settings-specific)
- Toggle switches (lines 49-54) use custom implementation
**Issues Found:**
- Storage migrate button disabled state unclear (opacity or true `disabled`?)
- Discord webhook input has no validation for URL format
- API key display shows preview only (first 8 + last 4) but no "copy" button
**UX Gaps:**
- Storage status shows badge but no tooltip explaining what each storage mode means
- Notification toggles don't persist immediately (need to understand async behavior)
- API check button shows output pre but max-height: 320px might truncate long output

---

### **setup.php** (200 lines)
**Layout:** Two-step wizard (detection + identity, then populate + checklist)
**Styling:**
- Inline `<style>` block: 77 lines (lines 1-77)
- Self-contained; no duplication of varaverk.css
- Monospace font for wizard (intentional)
**Issues Found:**
- Detection banner (line 87) shows "Detecting…" with no timeout—if API slow, stays visible
- Step 2 populate status (line 140) shows placeholder; if JS fails, stays visible
- Role button styling (lines 30-33) uses inline styles not classes
**UX Gaps:**
- Partner hostname field (line 115) required on Partner role but not validated before save
- Checklist links (line 161, 170) use `<a href="#" onclick="...">` not `<button>`
- No progress indicator showing 1/2 until step 2 starts

---

### **watchdog.php** (200+ lines, partial read)
**Layout:** Host rows, each with pressure alert, system card, docker card, stability card
**Styling:**
- Inline `<style>` block: 42 lines (lines 1-42)
- Uses `.vv-wd-*` classes (watchdog-specific)
- Heavy grid layout (lines 33-41) with media queries
**Issues Found:**
- `_relTime()`, `_fmtBytes()`, `_dur()` are EXACT duplicates from fallback and rsync
- Strike row display (line 23-25) uses inline color but no semantic meaning
- Pressure alert (lines 100-121) shows container names as pills but no status (running/paused/stopped)
**UX Gaps:**
- OOM kills count shows "0" in grey but no context (is this good?)
- Reboot timestamp (line 152) shown but no indication when next reboot scheduled
- Load avg shown as float (e.g., 1.23) but no context relative to core count visible in row

---

## SUMMARY TABLE

| Finding Category | Severity | Count | Details |
|------------------|----------|-------|---------|
| **Visual Inconsistencies** | HIGH | 8 | Toggle sizes, button colors, spacing, icons |
| **UX Gaps** | MEDIUM | 15 | Missing loading states, confirmations, empty states |
| **Inline Styles** | MEDIUM | 7 pages | Heavy duplication; consolidate to CSS |
| **Broken Flows** | LOW | 6 | Incomplete feedback, hard-coded positioning |
| **Cross-Page Duplication** | HIGH | 20+ | Shared functions (`_dur`, `_relTime`, `_fmtBytes`) repeated 3+ times |

### QUICK WINS (2–4 hour fixes)
1. Create `varaverk-utils.js` with shared formatters
2. Consolidate button color classes in varaverk.css
3. Extract modal system from auth.php to reusable component
4. Add loading states to all async operations
5. Replace `confirm()` with styled modal

### MEDIUM EFFORT (1 week refactor)
1. Move auth modal styling to varaverk.css
2. Unify toggle switch implementations (currently 5 variants)
3. Create error state patterns for all pages
4. Add empty-state UI components
5. Consolidate color values to CSS variables