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:
@@ -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
|
||||
// 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.
|
||||
// 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'] === '*');
|
||||
$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) {
|
||||
$name = (string) $g['subsection'];
|
||||
if (isset(VV_UI_SECTION_EXCLUDE[$name])) continue;
|
||||
|
||||
@@ -107,8 +107,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||
// 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
|
||||
// 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 === '*');
|
||||
$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 = [];
|
||||
foreach (vv_get_conf_files() as $f) {
|
||||
foreach (vv_conf_all_groups($f) as $g) {
|
||||
|
||||
@@ -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; }
|
||||
.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
|
||||
somewhere else having become enabled. */
|
||||
.vv-cf-field.dirty { border-left-color: #6a5228; background: #14110a; }
|
||||
|
||||
@@ -107,6 +107,20 @@ const VV_UI_SECTION_SURFACES = [
|
||||
'tab' => 'AI',
|
||||
'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
|
||||
// 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.
|
||||
|
||||
@@ -64,6 +64,42 @@
|
||||
// fatals on the one page that included this without it, which is what took the AI tab blank.
|
||||
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 {
|
||||
static $done = false;
|
||||
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>
|
||||
<?php
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
// DEPENDS ON
|
||||
// api/arrs.php polled every 60s → include/arrs.php
|
||||
// api/confform.php inline conf edits → include/confform.php
|
||||
require_once dirname(__DIR__) . '/include/confui.php';
|
||||
?>
|
||||
<style>
|
||||
/* ── 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>
|
||||
|
||||
|
||||
<?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>
|
||||
(function() {
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
// DEPENDS ON
|
||||
// api/docker.php polled every 60s → include/docker.php
|
||||
// api/docker_action.php start/stop/restart
|
||||
require_once dirname(__DIR__) . '/include/confui.php';
|
||||
?>
|
||||
<style>
|
||||
.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>
|
||||
|
||||
|
||||
<?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>
|
||||
(function() {
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
// DEPENDS ON
|
||||
// api/fallback.php polled every 30s → include/fallback.php
|
||||
// api/confform.php inline conf edits → include/confform.php
|
||||
require_once dirname(__DIR__) . '/include/confui.php';
|
||||
?>
|
||||
<style>
|
||||
/* ── Existing status styles ── */
|
||||
@@ -168,6 +169,13 @@
|
||||
</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>
|
||||
(function() {
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
// api/create_api_key.php credential provisioning
|
||||
// api/run.php long-running deploy operations
|
||||
// api/confform.php inline conf edits
|
||||
require_once dirname(__DIR__) . '/include/confui.php';
|
||||
?>
|
||||
<style>
|
||||
.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>
|
||||
|
||||
|
||||
<?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>
|
||||
// ── Toggle states — persist across 10s poll re-renders ────────────────────────
|
||||
const _vvOnboarding = {}; // hostId → true when onboard steps are expanded
|
||||
|
||||
@@ -31,6 +31,7 @@ those settings genuinely have no control, and saying so is the right answer.
|
||||
Reachable from:
|
||||
|
||||
- Scheduler tab → **Arrs Failed Stalled Recovery** → Config → *Arr Recovery Toggles*
|
||||
- Arrs tab → Arr settings → *Arr Recovery Toggles*
|
||||
- Settings tab → All settings → *Arr Recovery Toggles*
|
||||
|
||||
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:
|
||||
|
||||
- Scheduler tab → **Docker Daily Restart** → Config → *Docker Daily Restart*
|
||||
- Docker tab → Docker settings → *Docker Daily Restart*
|
||||
- Settings tab → All settings → *Docker Daily Restart*
|
||||
|
||||
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:
|
||||
|
||||
- Scheduler tab → **Docker Network Connect** → Config → *Docker Network Connect*
|
||||
- Docker tab → Docker settings → *Docker Network Connect*
|
||||
- Settings tab → All settings → *Docker Network Connect*
|
||||
|
||||
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:
|
||||
|
||||
- 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*
|
||||
|
||||
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:
|
||||
|
||||
- Scheduler tab → **Docker Weekly Restart** → Config → *Docker Weekly Restart*
|
||||
- Docker tab → Docker settings → *Docker Weekly Restart*
|
||||
- Settings tab → All settings → *Docker Weekly Restart*
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -320,6 +329,7 @@ Reachable from:
|
||||
- Scheduler tab → **Lidarr Duplicate Artist Cleanup** → Config → *Lidarr*
|
||||
- Scheduler tab → **Playback Aware Lidarr Discovery** → Config → *Lidarr*
|
||||
- Scheduler tab → **Emby To Lidarr Sync** → Config → *Lidarr*
|
||||
- Arrs tab → Arr settings → *Lidarr*
|
||||
- Settings tab → All settings → *Lidarr*
|
||||
|
||||
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:
|
||||
|
||||
- Scheduler tab → **Network Watchdog** → Config → *Network Watchdog*
|
||||
- Watchdog tab → Watchdog settings → *Network Watchdog*
|
||||
- Settings tab → All settings → *Network Watchdog*
|
||||
|
||||
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 → **Radarr Tmdb Removed** → Config → *Radarr*
|
||||
- Scheduler tab → **Emby To Radarr Sync** → Config → *Radarr*
|
||||
- Arrs tab → Arr settings → *Radarr*
|
||||
- Settings tab → All settings → *Radarr*
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -538,6 +553,7 @@ Saved into `host1.conf`, which does not need to be opened by hand.
|
||||
Reachable from:
|
||||
|
||||
- Scheduler tab → **Watchdog Orchestrator** → Config → *SYSTEM WATCHDOG*
|
||||
- Watchdog tab → Watchdog settings → *SYSTEM WATCHDOG*
|
||||
- Settings tab → All settings → *SYSTEM WATCHDOG*
|
||||
|
||||
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 → **Sonarr Tvdb Removed** → Config → *Sonarr*
|
||||
- Scheduler tab → **Emby To Sonarr Sync** → Config → *Sonarr*
|
||||
- Arrs tab → Arr settings → *Sonarr*
|
||||
- Settings tab → All settings → *Sonarr*
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -913,6 +933,7 @@ Reachable from:
|
||||
- Scheduler tab → **Radarr Cleanup** → Config → *Arr Cleanup*
|
||||
- Scheduler tab → **Lidarr Cleanup** → Config → *Arr Cleanup*
|
||||
- Scheduler tab → **Sonarr Cleanup** → Config → *Arr Cleanup*
|
||||
- Arrs tab → Arr settings → *Arr Cleanup*
|
||||
- Settings tab → All settings → *Arr Cleanup*
|
||||
|
||||
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 → **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)*
|
||||
|
||||
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:
|
||||
|
||||
- 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*
|
||||
|
||||
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:
|
||||
|
||||
- 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*
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -1209,6 +1236,8 @@ Saved into `master.conf`, which does not need to be opened by hand.
|
||||
Reachable from:
|
||||
|
||||
- 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*
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -1449,6 +1481,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
|
||||
Reachable from:
|
||||
|
||||
- Scheduler tab → **Network Watchdog** → Config → *Network Watchdog*
|
||||
- Watchdog tab → Watchdog settings → *Network Watchdog*
|
||||
- Settings tab → All settings → *Network Watchdog*
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -1646,6 +1682,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
|
||||
Reachable from:
|
||||
|
||||
- Scheduler tab → **Rsync** → Config → *Rsync Defaults*
|
||||
- Rsync tab → Rsync settings → *Rsync Defaults*
|
||||
- Settings tab → All settings → *Rsync Defaults*
|
||||
|
||||
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:
|
||||
|
||||
- Scheduler tab → **Rsync** → Config → *Rsync Enable/Disable*
|
||||
- Rsync tab → Rsync settings → *Rsync Enable/Disable*
|
||||
- Settings tab → All settings → *Rsync Enable/Disable*
|
||||
|
||||
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:
|
||||
|
||||
- Scheduler tab → **Rsync** → Config → *Rsync Merge Auto-Promote*
|
||||
- Rsync tab → Rsync 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.
|
||||
@@ -1700,6 +1739,7 @@ Saved into `master.conf`, which does not need to be opened by hand.
|
||||
Reachable from:
|
||||
|
||||
- Scheduler tab → **Rsync** → Config → *Rsync Profile System*
|
||||
- Rsync tab → Rsync settings → *Rsync Profile System*
|
||||
- Settings tab → All settings → *Rsync Profile System*
|
||||
|
||||
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:
|
||||
|
||||
- Scheduler tab → **Watchdog Orchestrator** → Config → *System Watchdog*
|
||||
- Watchdog tab → Watchdog settings → *System Watchdog*
|
||||
- Settings tab → All settings → *System Watchdog*
|
||||
|
||||
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:
|
||||
|
||||
- Scheduler tab → **Watchdog Orchestrator** → Config → *Watchdog Orchestrator*
|
||||
- Watchdog tab → Watchdog settings → *Watchdog Orchestrator*
|
||||
- Settings tab → All settings → *Watchdog Orchestrator*
|
||||
|
||||
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:
|
||||
|
||||
- Scheduler tab → **Webgui Watchdog** → Config → *WebGUI Watchdog*
|
||||
- Watchdog tab → Watchdog settings → *WebGUI Watchdog*
|
||||
- Settings tab → All settings → *WebGUI Watchdog*
|
||||
|
||||
Saved into `master.conf`, which does not need to be opened by hand.
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
// api/rsync_win_arrays.php share array editing
|
||||
// api/flag_toggle.php enable/disable toggles
|
||||
// api/confform.php inline conf edits
|
||||
require_once dirname(__DIR__) . '/include/confui.php';
|
||||
?>
|
||||
<style>
|
||||
.vv-ry-card { background:#161616;border:1px solid #2a2a2a;border-radius:6px;padding:10px 12px;min-width:0; }
|
||||
@@ -511,6 +512,13 @@
|
||||
|
||||
</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>
|
||||
// Shared state — globals so IIFE internals and onclick handlers can both reach them
|
||||
var _vvRyWinOpen = new Set(); // which window panels are expanded
|
||||
|
||||
@@ -87,20 +87,6 @@ vv_conf_ui_assets();
|
||||
font-size:12px;padding:5px 8px;outline:none;width:100%;box-sizing:border-box;
|
||||
font-family:monospace; }
|
||||
.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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Everything else the conf holds. The cards above are purpose-built for the few settings that
|
||||
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.
|
||||
<?php vv_conf_ui_card('vv-cf-all', '*', 'All settings'); ?>
|
||||
|
||||
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>
|
||||
@@ -530,111 +495,4 @@ function vvApiRenew() {
|
||||
// Auto-check on load
|
||||
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>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
//
|
||||
// DEPENDS ON
|
||||
// api/watchdog.php polled every 30s → include/watchdog.php
|
||||
require_once dirname(__DIR__) . '/include/confui.php';
|
||||
?>
|
||||
<style>
|
||||
.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>
|
||||
|
||||
|
||||
<?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>
|
||||
(function() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user