Put each page's settings on that page

A watchdog threshold was reachable only from the bottom of a list of a hundred
and twenty on another tab.
This commit is contained in:
Gmer4Lfe
2026-08-12 19:55:51 -04:00
parent 8ca52c64b9
commit e64d26af6c
13 changed files with 284 additions and 151 deletions
+6 -1
View File
@@ -71,8 +71,13 @@ foreach (VV_UI_SECTION_SURFACES as $surface) {
// "*" is the catch-all, matched the same way api/confform.php matches it. preg_quote would // "*" is the catch-all, matched the same way api/confform.php matches it. preg_quote would
// turn it into \* and quietly match nothing, which is how the map went on reporting a third // turn it into \* and quietly match nothing, which is how the map went on reporting a third
// of the conf as unreachable after the page that reaches it had shipped. // of the conf as unreachable after the page that reaches it had shipped.
// Same matching as api/confform.php, including the pipe-separated form — the map and the page
// must agree about which sections a surface shows, or the route it prints is fiction.
$all = ((string) $surface['match'] === '*'); $all = ((string) $surface['match'] === '*');
$re = $all ? '' : '/\b' . preg_quote((string) $surface['match'], '/') . '\b/i'; $re = $all ? '' : '/\b(' . implode('|', array_map(
fn($w) => preg_quote(trim($w), '/'),
array_filter(explode('|', (string) $surface['match']), fn($w) => trim($w) !== '')))
. ')\b/i';
foreach ($sections as $k => $g) { foreach ($sections as $k => $g) {
$name = (string) $g['subsection']; $name = (string) $g['subsection'];
if (isset(VV_UI_SECTION_EXCLUDE[$name])) continue; if (isset(VV_UI_SECTION_EXCLUDE[$name])) continue;
+7 -1
View File
@@ -107,8 +107,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// with boundaries around it rather than as a pattern a caller could widen to everything. // with boundaries around it rather than as a pattern a caller could widen to everything.
// "*" is every section this host has. Deliberately not the same as an empty needle, which // "*" is every section this host has. Deliberately not the same as an empty needle, which
// would match everything by accident rather than on purpose. // would match everything by accident rather than on purpose.
// Several words, pipe-separated, each still matched whole and still quoted as a literal.
// One word was not enough: the Arr sections are named Arr Sync, Lidarr, Sonarr and
// Radarr, and no single stem catches those without also catching "Array Start", which is
// the Unraid disk array and nothing to do with them.
$all = ($match === '*'); $all = ($match === '*');
$re = $all ? '' : '/\b' . preg_quote($match, '/') . '\b/i'; $re = $all ? '' : '/\b(' . implode('|', array_map(
fn($w) => preg_quote(trim($w), '/'),
array_filter(explode('|', $match), fn($w) => trim($w) !== ''))) . ')\b/i';
$out = []; $out = [];
foreach (vv_get_conf_files() as $f) { foreach (vv_get_conf_files() as $f) {
foreach (vv_conf_all_groups($f) as $g) { foreach (vv_conf_all_groups($f) as $g) {
+29
View File
@@ -559,6 +559,35 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
padding: 3px 8px; border-radius: 3px; cursor: pointer; flex-shrink: 0; } padding: 3px 8px; border-radius: 3px; cursor: pointer; flex-shrink: 0; }
.vv-cf-reveal:hover { color: #aaa; border-color: #4a4a4a; } .vv-cf-reveal:hover { color: #aaa; border-color: #4a4a4a; }
/* ── The settings card ────────────────────────────────────────────────────────
Emitted by vv_conf_ui_card(), so every page that shows settings shows the same object. Lives
here rather than in each page's inline block for the reason nine page-private vocabularies
existed in the first place: a card defined where it is used is a card that drifts. */
.vv-cf-card { background:#161616; border:1px solid #2a2a2a; border-radius:6px;
padding:12px 14px; margin-bottom:14px; }
.vv-cf-card-h { display:flex; align-items:center; gap:10px; cursor:pointer; user-select:none;
flex-wrap:wrap; }
.vv-cf-card-c { color:#333; font-size:9px; transition:transform .12s; }
.vv-cf-card-h.open .vv-cf-card-c { transform:rotate(90deg); }
.vv-cf-card-l { font-size:11px; font-weight:700; color:#666; text-transform:uppercase;
letter-spacing:.07em; }
.vv-cf-card-f { max-width:240px; cursor:text; background:#0d0d0d; border:1px solid #2a2a2a;
border-radius:3px; color:#bbb; font-size:12px; padding:4px 8px; outline:none;
font-family:inherit; }
.vv-cf-card-f:focus { border-color:#444; }
.vv-cf-card-s { margin-left:auto; font-size:10px; color:#444; font-family:monospace; }
.vv-cf-card-s.warn { color:#ffb74d; }
.vv-cf-card-s.ok { color:#4caf50; }
.vv-cf-card-s.bad { color:#ef5350; }
.vv-cf-card-save { background:#1a2a1a; border:1px solid #2d4a2d; color:#4caf50; font-size:11px;
padding:5px 14px; border-radius:3px; cursor:pointer; font-family:inherit; }
.vv-cf-card-save:hover:not(:disabled) { background:#223a22; }
.vv-cf-card-save:disabled { opacity:.4; cursor:default; }
/* Scrolls itself rather than growing the page past the viewport, so Save stays reachable. */
.vv-cf-card [id$="-fields"] { max-height:60vh; overflow-y:auto; margin-top:10px; padding-right:4px; }
/* Hidden by the filter, not removed — see the note in VvConfCard. */
.vv-cf-group.vv-filtered { display:none; }
/* Touched but not yet saved. The row says so itself rather than relying on a Save button /* Touched but not yet saved. The row says so itself rather than relying on a Save button
somewhere else having become enabled. */ somewhere else having become enabled. */
.vv-cf-field.dirty { border-left-color: #6a5228; background: #14110a; } .vv-cf-field.dirty { border-left-color: #6a5228; background: #14110a; }
+14
View File
@@ -107,6 +107,20 @@ const VV_UI_SECTION_SURFACES = [
'tab' => 'AI', 'tab' => 'AI',
'route' => 'AI tab → Settings → Configuration', 'route' => 'AI tab → Settings → Configuration',
], ],
// Subject pages first, so a setting named after the page you are already on is found there
// rather than at the bottom of a list of a hundred and twenty. The catch-all below still
// carries all of them, and the map lists both routes because both are true.
['match' => 'watchdog', 'tab' => 'Watchdog',
'route' => 'Watchdog tab → Watchdog settings'],
['match' => 'rsync', 'tab' => 'Rsync',
'route' => 'Rsync tab → Rsync settings'],
['match' => 'fallback|tier', 'tab' => 'Fallback',
'route' => 'Fallback tab → Fallback settings'],
['match' => 'arr|sonarr|radarr|lidarr', 'tab' => 'Arrs',
'route' => 'Arrs tab → Arr settings'],
['match' => 'docker', 'tab' => 'Docker',
'route' => 'Docker tab → Docker settings'],
// Everything that is not structurally excluded. The catch-all exists so no ordinary setting // Everything that is not structurally excluded. The catch-all exists so no ordinary setting
// is reachable only by editing a file — a settings page whose answer to a third of the conf // is reachable only by editing a file — a settings page whose answer to a third of the conf
// is "open it over SSH" is not a settings page. // is "open it over SSH" is not a settings page.
+130
View File
@@ -64,6 +64,42 @@
// fatals on the one page that included this without it, which is what took the AI tab blank. // fatals on the one page that included this without it, which is what took the AI tab blank.
require_once __DIR__ . '/confform.php'; require_once __DIR__ . '/confform.php';
// A whole settings card: disclosure, filter, save, and the fields for one subject. Emitted by a
// page in one line rather than assembled there.
//
// This exists because the alternative was six copies. The AI tab, the Scheduler and the Settings
// page each grew their own load/filter/dirty/save cycle, and the third one was written by copying
// the second — which is exactly how this plugin ended up with nine settings vocabularies and six
// toggles. Everything subtle here is subtle because it already cost something once: the filter
// hides rather than removes so a pending edit survives it, the save rebases instead of refetching
// so the field under the cursor does not repaint, and a refused write leaves the row dirty
// because it genuinely did not happen.
//
// $prefix composes every id, so a page may hold more than one card
// $match which sections — a whole word matched against section headers, or "*" for all
// $title the card's heading
function vv_conf_ui_card(string $prefix, string $match, string $title = 'Settings'): void {
vv_conf_ui_assets();
$p = htmlspecialchars($prefix, ENT_QUOTES);
?>
<div class="vv-cf-card" id="<?= $p ?>-card">
<div class="vv-cf-card-h" id="<?= $p ?>-t">
<span class="vv-cf-card-c">▶</span>
<span class="vv-cf-card-l"><?= htmlspecialchars($title) ?></span>
<input type="text" class="vv-cf-card-f" id="<?= $p ?>-filter"
placeholder="filter…" style="display:none">
<span class="vv-cf-card-s" id="<?= $p ?>-sum"></span>
<button class="vv-cf-card-save" id="<?= $p ?>-save" type="button"
style="display:none" disabled>Save</button>
</div>
<div id="<?= $p ?>-body" style="display:none">
<div id="<?= $p ?>-fields"><p class="vv-cf-empty">opens when you do</p></div>
</div>
</div>
<script>VvConfCard(<?= json_encode($prefix) ?>, <?= json_encode($match) ?>);</script>
<?php
}
function vv_conf_ui_assets(): void { function vv_conf_ui_assets(): void {
static $done = false; static $done = false;
if ($done) return; if ($done) return;
@@ -337,6 +373,100 @@ function vv_conf_ui_assets(): void {
}); });
}, },
}; };
// One card, self-contained. Everything a page used to write for itself.
window.VvConfCard = function (prefix, match) {
const $ = s => document.getElementById(prefix + '-' + s);
const API = '/plugins/varaverk/api/confform.php';
let loaded = false;
const dirty = () => {
const n = VvConfUI.dirtyCount(prefix + '-fields');
$('save').disabled = (n === 0);
$('sum').textContent = n ? (n + ' unsaved') : '';
$('sum').className = 'vv-cf-card-s' + (n ? ' warn' : '');
};
function load() {
if (loaded) return;
loaded = true;
fetch(API + '?sections=' + encodeURIComponent(match))
.then(r => r.json())
.then(d => {
if (!d.ok) throw new Error(d.error || 'could not be read');
VvConfUI.render(prefix + '-fields', d.groups || []);
VvConfUI.wire(prefix + '-fields', dirty);
dirty();
})
.catch(e => {
// Retryable — closing and reopening tries again rather than leaving a permanent error.
loaded = false;
$('fields').innerHTML = '<p class="vv-cf-empty">could not be read: '
+ String(e.message || e) + '</p>';
});
}
$('t').addEventListener('click', e => {
// The filter and the save live in the header; clicking either must not collapse the card.
if (e.target.closest('#' + prefix + '-filter, #' + prefix + '-save')) return;
const open = $('body').style.display === 'none';
$('body').style.display = open ? '' : 'none';
$('t').classList.toggle('open', open);
$('filter').style.display = open ? '' : 'none';
$('save').style.display = open ? '' : 'none';
// First open only, so collapsing to look at something else does not discard edits.
if (open) load();
});
// Hides whole sections, never individual fields — a setting means little without the heading
// saying what it belongs to. display:none rather than removal, so a field edited before
// filtering is still dirty and still saved: a filter that silently dropped pending edits
// would be a data-loss bug wearing a search box.
$('filter').addEventListener('input', () => {
const q = $('filter').value.trim().toLowerCase();
$('fields').querySelectorAll('.vv-cf-group').forEach(g => {
g.classList.toggle('vv-filtered', q !== '' && !g.textContent.toLowerCase().includes(q));
});
});
$('save').addEventListener('click', () => {
const changes = VvConfUI.collect(prefix + '-fields');
if (!changes.length) return;
$('save').disabled = true;
$('sum').textContent = 'saving…';
$('sum').className = 'vv-cf-card-s';
// URLSearchParams, not FormData — a multipart POST to this plugin's endpoints hangs with
// no status ever returned.
fetch(API, { method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: new URLSearchParams({ changes: JSON.stringify(changes) }) })
.then(r => r.text())
.then(t => {
let d;
try { d = JSON.parse(t); }
catch (_) {
throw new Error(t.trim() ? 'unparseable response'
: 'empty response — rejected before the endpoint ran');
}
if (!d.ok) throw new Error(d.error || 'save failed');
// Rebased, not refetched: the values on screen are the values on disk now, and a
// reload would repaint the control under the cursor.
VvConfUI.commit(prefix + '-fields');
dirty();
$('sum').textContent = 'saved ' + changes.length
+ ' change' + (changes.length > 1 ? 's' : '');
$('sum').className = 'vv-cf-card-s ok';
})
.catch(e => {
// Left dirty on purpose. A refused write changed nothing, and clearing the marks
// would say it had.
$('sum').textContent = String(e.message || e);
$('sum').className = 'vv-cf-card-s bad';
dirty();
});
});
};
})(); })();
</script> </script>
<?php <?php
+8
View File
@@ -25,6 +25,7 @@
// DEPENDS ON // DEPENDS ON
// api/arrs.php polled every 60s → include/arrs.php // api/arrs.php polled every 60s → include/arrs.php
// api/confform.php inline conf edits → include/confform.php // api/confform.php inline conf edits → include/confform.php
require_once dirname(__DIR__) . '/include/confui.php';
?> ?>
<style> <style>
/* ── Cards ───────────────────────────────────────────────── */ /* ── Cards ───────────────────────────────────────────────── */
@@ -114,6 +115,13 @@
<div style="grid-column:1/-1;color:#444;font-size:12px;padding:24px 0;text-align:center;">Loading…</div> <div style="grid-column:1/-1;color:#444;font-size:12px;padding:24px 0;text-align:center;">Loading…</div>
</div> </div>
<?php
// The conf sections this page is about, drawn by the shared renderer. They were reachable
// only from the Settings tab's catch-all, which is a long way to go for a setting named
// after the page you are already looking at.
vv_conf_ui_card('vv-cf-arrs', 'arr|sonarr|radarr|lidarr', 'Arr settings');
?>
<script> <script>
(function() { (function() {
+8
View File
@@ -25,6 +25,7 @@
// DEPENDS ON // DEPENDS ON
// api/docker.php polled every 60s → include/docker.php // api/docker.php polled every 60s → include/docker.php
// api/docker_action.php start/stop/restart // api/docker_action.php start/stop/restart
require_once dirname(__DIR__) . '/include/confui.php';
?> ?>
<style> <style>
.vv-dk-toolbar { display:flex;align-items:center;gap:8px;margin-bottom:12px;padding:0 2px;flex-wrap:wrap; } .vv-dk-toolbar { display:flex;align-items:center;gap:8px;margin-bottom:12px;padding:0 2px;flex-wrap:wrap; }
@@ -133,6 +134,13 @@
<div class="vv-dk-popover" id="vv-dk-popover" style="display:none;"></div> <div class="vv-dk-popover" id="vv-dk-popover" style="display:none;"></div>
<?php
// The conf sections this page is about, drawn by the shared renderer. They were reachable
// only from the Settings tab's catch-all, which is a long way to go for a setting named
// after the page you are already looking at.
vv_conf_ui_card('vv-cf-docker', 'docker', 'Docker settings');
?>
<script> <script>
(function() { (function() {
+8
View File
@@ -29,6 +29,7 @@
// DEPENDS ON // DEPENDS ON
// api/fallback.php polled every 30s → include/fallback.php // api/fallback.php polled every 30s → include/fallback.php
// api/confform.php inline conf edits → include/confform.php // api/confform.php inline conf edits → include/confform.php
require_once dirname(__DIR__) . '/include/confui.php';
?> ?>
<style> <style>
/* ── Existing status styles ── */ /* ── Existing status styles ── */
@@ -168,6 +169,13 @@
</div> </div>
</div> </div>
<?php
// The conf sections this page is about, drawn by the shared renderer. They were reachable
// only from the Settings tab's catch-all, which is a long way to go for a setting named
// after the page you are already looking at.
vv_conf_ui_card('vv-cf-fallback', 'fallback|tier', 'Fallback settings');
?>
<script> <script>
(function() { (function() {
+8
View File
@@ -35,6 +35,7 @@
// api/create_api_key.php credential provisioning // api/create_api_key.php credential provisioning
// api/run.php long-running deploy operations // api/run.php long-running deploy operations
// api/confform.php inline conf edits // api/confform.php inline conf edits
require_once dirname(__DIR__) . '/include/confui.php';
?> ?>
<style> <style>
.vv-pt-grid { display:grid; gap:12px; } .vv-pt-grid { display:grid; gap:12px; }
@@ -157,6 +158,13 @@ textarea.vv-pt-set-input { resize:vertical; white-space:pre; }
<div id="vv-pt-settings-body" style="display:none;margin-top:10px;"></div> <div id="vv-pt-settings-body" style="display:none;margin-top:10px;"></div>
</div> </div>
<?php
// The conf sections this page is about, drawn by the shared renderer. They were reachable
// only from the Settings tab's catch-all, which is a long way to go for a setting named
// after the page you are already looking at.
vv_conf_ui_card('vv-cf-partnership', 'partnership', 'Partnership settings');
?>
<script> <script>
// ── Toggle states — persist across 10s poll re-renders ──────────────────────── // ── Toggle states — persist across 10s poll re-renders ────────────────────────
const _vvOnboarding = {}; // hostId → true when onboard steps are expanded const _vvOnboarding = {}; // hostId → true when onboard steps are expanded
+49 -6
View File
@@ -31,6 +31,7 @@ those settings genuinely have no control, and saying so is the right answer.
Reachable from: Reachable from:
- Scheduler tab → **Arrs Failed Stalled Recovery** → Config → *Arr Recovery Toggles* - Scheduler tab → **Arrs Failed Stalled Recovery** → Config → *Arr Recovery Toggles*
- Arrs tab → Arr settings → *Arr Recovery Toggles*
- Settings tab → All settings → *Arr Recovery Toggles* - Settings tab → All settings → *Arr Recovery Toggles*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -138,6 +139,7 @@ Saved into `host1.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Docker Daily Restart** → Config → *Docker Daily Restart* - Scheduler tab → **Docker Daily Restart** → Config → *Docker Daily Restart*
- Docker tab → Docker settings → *Docker Daily Restart*
- Settings tab → All settings → *Docker Daily Restart* - Settings tab → All settings → *Docker Daily Restart*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -151,6 +153,7 @@ Saved into `host1.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Docker Network Connect** → Config → *Docker Network Connect* - Scheduler tab → **Docker Network Connect** → Config → *Docker Network Connect*
- Docker tab → Docker settings → *Docker Network Connect*
- Settings tab → All settings → *Docker Network Connect* - Settings tab → All settings → *Docker Network Connect*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -165,6 +168,8 @@ Saved into `host1.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Docker Watchdog** → Config → *Docker Watchdog* - Scheduler tab → **Docker Watchdog** → Config → *Docker Watchdog*
- Watchdog tab → Watchdog settings → *Docker Watchdog*
- Docker tab → Docker settings → *Docker Watchdog*
- Settings tab → All settings → *Docker Watchdog* - Settings tab → All settings → *Docker Watchdog*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -184,6 +189,7 @@ Saved into `host1.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Docker Weekly Restart** → Config → *Docker Weekly Restart* - Scheduler tab → **Docker Weekly Restart** → Config → *Docker Weekly Restart*
- Docker tab → Docker settings → *Docker Weekly Restart*
- Settings tab → All settings → *Docker Weekly Restart* - Settings tab → All settings → *Docker Weekly Restart*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -238,7 +244,10 @@ Saved into `host1.conf`, which does not need to be opened by hand.
## Fallback Tiers — What HOST1 Wants Covered When Down ## Fallback Tiers — What HOST1 Wants Covered When Down
Route: Settings tab → All settings → *Fallback Tiers — What HOST1 Wants Covered When Down* Reachable from:
- Fallback tab → Fallback settings → *Fallback Tiers — What HOST1 Wants Covered When Down*
- Settings tab → All settings → *Fallback Tiers — What HOST1 Wants Covered When Down*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -320,6 +329,7 @@ Reachable from:
- Scheduler tab → **Lidarr Duplicate Artist Cleanup** → Config → *Lidarr* - Scheduler tab → **Lidarr Duplicate Artist Cleanup** → Config → *Lidarr*
- Scheduler tab → **Playback Aware Lidarr Discovery** → Config → *Lidarr* - Scheduler tab → **Playback Aware Lidarr Discovery** → Config → *Lidarr*
- Scheduler tab → **Emby To Lidarr Sync** → Config → *Lidarr* - Scheduler tab → **Emby To Lidarr Sync** → Config → *Lidarr*
- Arrs tab → Arr settings → *Lidarr*
- Settings tab → All settings → *Lidarr* - Settings tab → All settings → *Lidarr*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -377,6 +387,7 @@ Saved into `host1.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Network Watchdog** → Config → *Network Watchdog* - Scheduler tab → **Network Watchdog** → Config → *Network Watchdog*
- Watchdog tab → Watchdog settings → *Network Watchdog*
- Settings tab → All settings → *Network Watchdog* - Settings tab → All settings → *Network Watchdog*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -489,6 +500,7 @@ Reachable from:
- Scheduler tab → **Playback Aware Radarr Discovery** → Config → *Radarr* - Scheduler tab → **Playback Aware Radarr Discovery** → Config → *Radarr*
- Scheduler tab → **Radarr Tmdb Removed** → Config → *Radarr* - Scheduler tab → **Radarr Tmdb Removed** → Config → *Radarr*
- Scheduler tab → **Emby To Radarr Sync** → Config → *Radarr* - Scheduler tab → **Emby To Radarr Sync** → Config → *Radarr*
- Arrs tab → Arr settings → *Radarr*
- Settings tab → All settings → *Radarr* - Settings tab → All settings → *Radarr*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -508,7 +520,10 @@ Saved into `host1.conf`, which does not need to be opened by hand.
## Rsync Writeback ## Rsync Writeback
Route: Settings tab → All settings → *Rsync Writeback* Reachable from:
- Rsync tab → Rsync settings → *Rsync Writeback*
- Settings tab → All settings → *Rsync Writeback*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -538,6 +553,7 @@ Saved into `host1.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Watchdog Orchestrator** → Config → *SYSTEM WATCHDOG* - Scheduler tab → **Watchdog Orchestrator** → Config → *SYSTEM WATCHDOG*
- Watchdog tab → Watchdog settings → *SYSTEM WATCHDOG*
- Settings tab → All settings → *SYSTEM WATCHDOG* - Settings tab → All settings → *SYSTEM WATCHDOG*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -573,6 +589,7 @@ Reachable from:
- Scheduler tab → **Playback Aware Sonarr Discovery** → Config → *Sonarr* - Scheduler tab → **Playback Aware Sonarr Discovery** → Config → *Sonarr*
- Scheduler tab → **Sonarr Tvdb Removed** → Config → *Sonarr* - Scheduler tab → **Sonarr Tvdb Removed** → Config → *Sonarr*
- Scheduler tab → **Emby To Sonarr Sync** → Config → *Sonarr* - Scheduler tab → **Emby To Sonarr Sync** → Config → *Sonarr*
- Arrs tab → Arr settings → *Sonarr*
- Settings tab → All settings → *Sonarr* - Settings tab → All settings → *Sonarr*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -615,7 +632,10 @@ Saved into `host1.conf`, which does not need to be opened by hand.
## Tier Delays — HOST1 Outage Timers ## Tier Delays — HOST1 Outage Timers
Route: Settings tab → All settings → *Tier Delays — HOST1 Outage Timers* Reachable from:
- Fallback tab → Fallback settings → *Tier Delays — HOST1 Outage Timers*
- Settings tab → All settings → *Tier Delays — HOST1 Outage Timers*
Saved into `host1.conf`, which does not need to be opened by hand. Saved into `host1.conf`, which does not need to be opened by hand.
@@ -913,6 +933,7 @@ Reachable from:
- Scheduler tab → **Radarr Cleanup** → Config → *Arr Cleanup* - Scheduler tab → **Radarr Cleanup** → Config → *Arr Cleanup*
- Scheduler tab → **Lidarr Cleanup** → Config → *Arr Cleanup* - Scheduler tab → **Lidarr Cleanup** → Config → *Arr Cleanup*
- Scheduler tab → **Sonarr Cleanup** → Config → *Arr Cleanup* - Scheduler tab → **Sonarr Cleanup** → Config → *Arr Cleanup*
- Arrs tab → Arr settings → *Arr Cleanup*
- Settings tab → All settings → *Arr Cleanup* - Settings tab → All settings → *Arr Cleanup*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1000,6 +1021,7 @@ Reachable from:
- Scheduler tab → **Radarr Classification Scan** → Config → *Arr Content Classification (radarr/sonarr_classification_scan.sh)* - Scheduler tab → **Radarr Classification Scan** → Config → *Arr Content Classification (radarr/sonarr_classification_scan.sh)*
- Scheduler tab → **Sonarr Classification Scan** → Config → *Arr Content Classification (radarr/sonarr_classification_scan.sh)* - Scheduler tab → **Sonarr Classification Scan** → Config → *Arr Content Classification (radarr/sonarr_classification_scan.sh)*
- Arrs tab → Arr settings → *Arr Content Classification (radarr/sonarr_classification_scan.sh)*
- Settings tab → All settings → *Arr Content Classification (radarr/sonarr_classification_scan.sh)* - Settings tab → All settings → *Arr Content Classification (radarr/sonarr_classification_scan.sh)*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1017,6 +1039,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Arrs Failed Stalled Recovery** → Config → *Arr Failed/Stalled Recovery* - Scheduler tab → **Arrs Failed Stalled Recovery** → Config → *Arr Failed/Stalled Recovery*
- Arrs tab → Arr settings → *Arr Failed/Stalled Recovery*
- Settings tab → All settings → *Arr Failed/Stalled Recovery* - Settings tab → All settings → *Arr Failed/Stalled Recovery*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1033,6 +1056,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Arr Full Rescan** → Config → *Arr Full Library Rescan* - Scheduler tab → **Arr Full Rescan** → Config → *Arr Full Library Rescan*
- Arrs tab → Arr settings → *Arr Full Library Rescan*
- Settings tab → All settings → *Arr Full Library Rescan* - Settings tab → All settings → *Arr Full Library Rescan*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1043,7 +1067,10 @@ Saved into `master.conf`, which does not need to be opened by hand.
## Arr Sync ## Arr Sync
Route: Settings tab → All settings → *Arr Sync* Reachable from:
- Arrs tab → Arr settings → *Arr Sync*
- Settings tab → All settings → *Arr Sync*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1209,6 +1236,8 @@ Saved into `master.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Docker Watchdog** → Config → *Docker Watchdog* - Scheduler tab → **Docker Watchdog** → Config → *Docker Watchdog*
- Watchdog tab → Watchdog settings → *Docker Watchdog*
- Docker tab → Docker settings → *Docker Watchdog*
- Settings tab → All settings → *Docker Watchdog* - Settings tab → All settings → *Docker Watchdog*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1304,7 +1333,10 @@ Saved into `master.conf`, which does not need to be opened by hand.
## FALLBACK ## FALLBACK
Route: Settings tab → All settings → *FALLBACK* Reachable from:
- Fallback tab → Fallback settings → *FALLBACK*
- Settings tab → All settings → *FALLBACK*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1449,6 +1481,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Network Watchdog** → Config → *Network Watchdog* - Scheduler tab → **Network Watchdog** → Config → *Network Watchdog*
- Watchdog tab → Watchdog settings → *Network Watchdog*
- Settings tab → All settings → *Network Watchdog* - Settings tab → All settings → *Network Watchdog*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1623,7 +1656,10 @@ Saved into `master.conf`, which does not need to be opened by hand.
## Remote Docker Daemon ## Remote Docker Daemon
Route: Settings tab → All settings → *Remote Docker Daemon* Reachable from:
- Docker tab → Docker settings → *Remote Docker Daemon*
- Settings tab → All settings → *Remote Docker Daemon*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1646,6 +1682,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Rsync** → Config → *Rsync Defaults* - Scheduler tab → **Rsync** → Config → *Rsync Defaults*
- Rsync tab → Rsync settings → *Rsync Defaults*
- Settings tab → All settings → *Rsync Defaults* - Settings tab → All settings → *Rsync Defaults*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1668,6 +1705,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Rsync** → Config → *Rsync Enable/Disable* - Scheduler tab → **Rsync** → Config → *Rsync Enable/Disable*
- Rsync tab → Rsync settings → *Rsync Enable/Disable*
- Settings tab → All settings → *Rsync Enable/Disable* - Settings tab → All settings → *Rsync Enable/Disable*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1687,6 +1725,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Rsync** → Config → *Rsync Merge Auto-Promote* - Scheduler tab → **Rsync** → Config → *Rsync Merge Auto-Promote*
- Rsync tab → Rsync settings → *Rsync Merge Auto-Promote*
- Settings tab → All settings → *Rsync Merge Auto-Promote* - Settings tab → All settings → *Rsync Merge Auto-Promote*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1700,6 +1739,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Rsync** → Config → *Rsync Profile System* - Scheduler tab → **Rsync** → Config → *Rsync Profile System*
- Rsync tab → Rsync settings → *Rsync Profile System*
- Settings tab → All settings → *Rsync Profile System* - Settings tab → All settings → *Rsync Profile System*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1806,6 +1846,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Watchdog Orchestrator** → Config → *System Watchdog* - Scheduler tab → **Watchdog Orchestrator** → Config → *System Watchdog*
- Watchdog tab → Watchdog settings → *System Watchdog*
- Settings tab → All settings → *System Watchdog* - Settings tab → All settings → *System Watchdog*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1891,6 +1932,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Watchdog Orchestrator** → Config → *Watchdog Orchestrator* - Scheduler tab → **Watchdog Orchestrator** → Config → *Watchdog Orchestrator*
- Watchdog tab → Watchdog settings → *Watchdog Orchestrator*
- Settings tab → All settings → *Watchdog Orchestrator* - Settings tab → All settings → *Watchdog Orchestrator*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
@@ -1904,6 +1946,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
Reachable from: Reachable from:
- Scheduler tab → **Webgui Watchdog** → Config → *WebGUI Watchdog* - Scheduler tab → **Webgui Watchdog** → Config → *WebGUI Watchdog*
- Watchdog tab → Watchdog settings → *WebGUI Watchdog*
- Settings tab → All settings → *WebGUI Watchdog* - Settings tab → All settings → *WebGUI Watchdog*
Saved into `master.conf`, which does not need to be opened by hand. Saved into `master.conf`, which does not need to be opened by hand.
+8
View File
@@ -36,6 +36,7 @@
// api/rsync_win_arrays.php share array editing // api/rsync_win_arrays.php share array editing
// api/flag_toggle.php enable/disable toggles // api/flag_toggle.php enable/disable toggles
// api/confform.php inline conf edits // api/confform.php inline conf edits
require_once dirname(__DIR__) . '/include/confui.php';
?> ?>
<style> <style>
.vv-ry-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px 12px;min-width:0; } .vv-ry-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px 12px;min-width:0; }
@@ -511,6 +512,13 @@
</div><!-- /layer --> </div><!-- /layer -->
<?php
// The conf sections this page is about, drawn by the shared renderer. They were reachable
// only from the Settings tab's catch-all, which is a long way to go for a setting named
// after the page you are already looking at.
vv_conf_ui_card('vv-cf-rsync', 'rsync', 'Rsync settings');
?>
<script> <script>
// Shared state — globals so IIFE internals and onclick handlers can both reach them // Shared state — globals so IIFE internals and onclick handlers can both reach them
var _vvRyWinOpen = new Set(); // which window panels are expanded var _vvRyWinOpen = new Set(); // which window panels are expanded
+1 -143
View File
@@ -87,20 +87,6 @@ vv_conf_ui_assets();
font-size:12px;padding:5px 8px;outline:none;width:100%;box-sizing:border-box; font-size:12px;padding:5px 8px;outline:none;width:100%;box-sizing:border-box;
font-family:monospace; } font-family:monospace; }
.vv-set-inp:focus{ border-color:#444; } .vv-set-inp:focus{ border-color:#444; }
/* The All-settings header. A row rather than a heading because it carries the disclosure, the
filter and the save together — the filter is only meaningful while the card is open, and the
save only while something is dirty, so both appear with what they act on. */
.vv-cf-all-h { display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none;
flex-wrap:wrap; }
.vv-cf-all-c { color:#333;font-size:9px;transition:transform .12s; }
.vv-cf-all-h.open .vv-cf-all-c { transform:rotate(90deg); }
.vv-cf-all-f { max-width:260px;cursor:text; }
/* Scrolls itself rather than growing the page past the viewport — 124 sections is far taller
than a screen, and the Save button belongs where it can still be reached. */
#vv-cf-all { max-height:60vh;overflow-y:auto;margin-top:10px;padding-right:4px; }
/* Hidden by the filter. display:none rather than removal, so the controls keep their state and
a field edited before filtering is still dirty and still saved. */
.vv-cf-group.vv-filtered { display:none; }
</style> </style>
<div style="max-width:740px;margin:0 auto;"> <div style="max-width:740px;margin:0 auto;">
@@ -238,29 +224,8 @@ vv_conf_ui_assets();
<pre class="vv-set-out" id="vv-api-out"></pre> <pre class="vv-set-out" id="vv-api-out"></pre>
</div> </div>
<!-- Everything else the conf holds. The cards above are purpose-built for the few settings that <?php vv_conf_ui_card('vv-cf-all', '*', 'All settings'); ?>
needed a workflow — a migration, a renewal, a webhook test. This is the rest: a third of the
configuration had no control anywhere, so the answer to "where do I change X" was "open a
file over SSH", which is not an answer a settings page should give.
Collapsed and loaded on first open. 124 sections is real work to assemble and the card is
shut almost every time the page is drawn.
Five sections are deliberately absent — the machine's identity and the roots everything
derives from. See VV_UI_SECTION_EXCLUDE for which, and why each one. -->
<div class="vv-set-card">
<div class="vv-cf-all-h" id="vv-cf-all-t">
<span class="vv-cf-all-c">▶</span>
<span class="vv-set-hdr" style="margin-bottom:0;">All settings</span>
<input type="text" class="vv-set-inp vv-cf-all-f" id="vv-cf-all-filter"
placeholder="filter by name or setting…" style="display:none">
<span class="vv-set-sum" id="vv-cf-all-sum" style="margin-left:auto"></span>
<button class="vv-set-btn primary" id="vv-cf-all-save" type="button"
style="display:none" disabled>Save</button>
</div>
<div id="vv-cf-all-body" style="display:none">
<div id="vv-cf-all"><p class="vv-cf-empty">opens when you do</p></div>
</div>
</div> </div>
</div> </div>
@@ -530,111 +495,4 @@ function vvApiRenew() {
// Auto-check on load // Auto-check on load
vvApiCheck(); vvApiCheck();
// ── All settings ─────────────────────────────────────────────────────────────
// Every conf section that is not structurally excluded, drawn by the same renderer the AI tab
// and the Scheduler use and written through the same guarded path. Nothing about which sections
// exist or what control each field gets is decided here — it is read from the conf.
const VV_CF_ALL_API = '/plugins/varaverk/api/confform.php';
let _vvCfAllLoaded = false;
function vvCfAllToggle() {
const head = document.getElementById('vv-cf-all-t');
const body = document.getElementById('vv-cf-all-body');
const open = body.style.display === 'none';
body.style.display = open ? '' : 'none';
head.classList.toggle('open', open);
document.getElementById('vv-cf-all-filter').style.display = open ? '' : 'none';
document.getElementById('vv-cf-all-save').style.display = open ? '' : 'none';
// First open only. Reloading on every toggle would discard edits in progress the moment
// someone collapsed the card to look at something else.
if (open) vvCfAllLoad();
}
function vvCfAllLoad() {
if (_vvCfAllLoaded) return;
_vvCfAllLoaded = true;
fetch(VV_CF_ALL_API + '?sections=*')
.then(r => r.json())
.then(d => {
if (!d.ok) throw new Error(d.error || 'could not be read');
VvConfUI.render('vv-cf-all', d.groups || []);
VvConfUI.wire('vv-cf-all', vvCfAllDirty);
vvCfAllDirty();
})
.catch(e => {
// Retryable: the flag goes back, so closing and reopening tries again rather than
// leaving a permanent error where the settings should be.
_vvCfAllLoaded = false;
document.getElementById('vv-cf-all').innerHTML =
'<p class="vv-cf-empty">could not be read: ' + String(e.message || e) + '</p>';
});
}
function vvCfAllDirty() {
const n = VvConfUI.dirtyCount('vv-cf-all');
const btn = document.getElementById('vv-cf-all-save');
const sum = document.getElementById('vv-cf-all-sum');
btn.disabled = (n === 0);
sum.textContent = n ? (n + ' unsaved') : '';
sum.style.color = n ? '#ffb74d' : '#444';
}
// Hides whole sections, never individual fields — a setting means little without the header that
// says what it belongs to. Matches the section name and the key names inside it, because people
// look for both. display:none rather than removal, so a field edited before filtering is still
// dirty and still saved: a filter that silently drops pending edits would be a data-loss bug
// wearing a search box.
function vvCfAllFilter() {
const q = document.getElementById('vv-cf-all-filter').value.trim().toLowerCase();
document.querySelectorAll('#vv-cf-all .vv-cf-group').forEach(g => {
g.classList.toggle('vv-filtered', q !== '' && !g.textContent.toLowerCase().includes(q));
});
}
function vvCfAllSave() {
const changes = VvConfUI.collect('vv-cf-all');
if (!changes.length) return;
const btn = document.getElementById('vv-cf-all-save');
const sum = document.getElementById('vv-cf-all-sum');
btn.disabled = true;
sum.textContent = 'saving…';
sum.style.color = '#444';
// URLSearchParams, not FormData — a multipart POST to this plugin's endpoints hangs with no
// status ever returned.
fetch(VV_CF_ALL_API, { method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
body: new URLSearchParams({ changes: JSON.stringify(changes) }) })
.then(r => r.text())
.then(t => {
let d;
try { d = JSON.parse(t); }
catch (_) {
throw new Error(t.trim() ? 'unparseable response'
: 'empty response — rejected before the endpoint ran');
}
if (!d.ok) throw new Error(d.error || 'save failed');
// Rebased rather than refetched: the values on screen are now the values on disk, and a
// reload would repaint every control including the one just edited.
VvConfUI.commit('vv-cf-all');
vvCfAllDirty();
sum.textContent = 'saved ' + changes.length + ' change' + (changes.length > 1 ? 's' : '');
sum.style.color = '#4caf50';
})
.catch(e => {
// Left dirty on purpose. A refused write changed nothing, and clearing the marks would
// say it had.
sum.textContent = String(e.message || e);
sum.style.color = '#ef5350';
vvCfAllDirty();
});
}
document.getElementById('vv-cf-all-t').addEventListener('click', e => {
// The filter and the save live in the header; clicking either must not collapse the card.
if (e.target.closest('#vv-cf-all-filter, #vv-cf-all-save')) return;
vvCfAllToggle();
});
document.getElementById('vv-cf-all-filter').addEventListener('input', vvCfAllFilter);
document.getElementById('vv-cf-all-save').addEventListener('click', vvCfAllSave);
</script> </script>
+8
View File
@@ -25,6 +25,7 @@
// //
// DEPENDS ON // DEPENDS ON
// api/watchdog.php polled every 30s → include/watchdog.php // api/watchdog.php polled every 30s → include/watchdog.php
require_once dirname(__DIR__) . '/include/confui.php';
?> ?>
<style> <style>
.vv-wd-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px;min-width:0; } .vv-wd-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px;min-width:0; }
@@ -78,6 +79,13 @@
<div style="grid-column:1/-1;color:#444;font-size:12px;padding:16px 0;text-align:center;">Loading…</div> <div style="grid-column:1/-1;color:#444;font-size:12px;padding:16px 0;text-align:center;">Loading…</div>
</div> </div>
<?php
// The conf sections this page is about, drawn by the shared renderer. They were reachable
// only from the Settings tab's catch-all, which is a long way to go for a setting named
// after the page you are already looking at.
vv_conf_ui_card('vv-cf-watchdog', 'watchdog', 'Watchdog settings');
?>
<script> <script>
(function() { (function() {